ReentrantLock:解锁Java并发编程的终极武器
欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。
1. 从synchronized的局限说起
在Java并发编程的早期,synchronized关键字是线程同步的唯一选择。它简单易用,JVM自动管理锁的获取和释放,但正是这种“自动化”带来了诸多限制:
public synchronized void transfer(Account from, Account to, int amount) {
// 如果这里发生死锁,线程将永远阻塞
from.withdraw(amount);
to.deposit(amount);
}
synchronized的三大痛点:
-
无法中断:线程获取锁失败会一直阻塞,无法响应中断请求
-
非公平锁:默认采用非公平策略,可能导致线程饥饿
-
单一条件队列:所有等待线程都在同一个队列中,唤醒不够精准
2. ReentrantLock的登场:更灵活的并发控制
JDK 1.5引入的java.util.concurrent包带来了ReentrantLock,它基于AQS(AbstractQueuedSynchronizer)框架实现,提供了更丰富的功能。
2.1 基础使用:手动控制的艺术
public class Account {
private final ReentrantLock lock = new ReentrantLock();
private int balance;
public void deposit(int amount) {
lock.lock(); // 手动获取锁
try {
balance += amount;
System.out.println(Thread.currentThread().getName() +
" 存入: " + amount + ",余额: " + balance);
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}
}
关键要点:
-
lock()和unlock()必须成对出现 -
unlock()必须放在finally块中,确保异常时也能释放锁 -
相比
synchronized的自动管理,ReentrantLock需要开发者更谨慎
2.2 可重入性:同一线程的多次加锁
可重入锁允许同一个线程多次获取同一把锁,这是避免死锁的关键特性。
public class ReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();
public void outerMethod() {
lock.lock(); // 第一次加锁,state从0→1
try {
System.out.println("Outer method acquired lock");
innerMethod(); // 调用内部方法,再次加锁
} finally {
lock.unlock(); // 最终释放锁
}
}
private void innerMethod() {
lock.lock(); // 第二次加锁,state从1→2(可重入)
try {
System.out.println("Inner method re-acquired lock");
} finally {
lock.unlock(); // state从2→1
}
}
}
底层原理:通过AQS的state变量记录重入次数,exclusiveOwnerThread记录当前持有锁的线程。
3. 深入AQS:并发框架的基石
3.1 AQS的核心结构
AQS(抽象队列同步器)是Java并发包的灵魂,采用模板方法模式设计。
| 组件 | 作用 | 说明 |
|---|---|---|
state | 同步状态 | volatile int类型,不同同步器有不同含义 |
| CLH队列 | 线程等待队列 | FIFO双向链表,保证公平性 |
Node | 队列节点 | 封装线程信息和等待状态 |
在ReentrantLock中,state的含义是:
-
state = 0:锁空闲 -
state = 1:锁被占用 -
state > 1:锁被重入,数值表示重入次数
3.2 加锁流程解析
非公平锁(默认)流程:
-
线程调用
lock()方法 -
直接尝试CAS将
state从0改为1 -
成功:获取锁,记录当前线程为持有者
-
失败:调用AQS的
acquire(1)入队等待
公平锁流程:
-
线程调用
lock()方法 -
先检查队列是否有等待线程(
hasQueuedPredecessors()) -
有等待线程:直接入队
-
无等待线程:尝试CAS获取锁
公平锁的核心代码:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键区别:先检查队列
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ... 可重入逻辑
}
4. 公平锁 vs 非公平锁:性能与公平的权衡
4.1 性能对比
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 严格按照FIFO顺序 | 允许插队,性能更高 |
| 吞吐量 | 较低(约低20%-30%) | 较高 |
| 适用场景 | 对公平性要求高的系统 | 高并发性能优先 |
实际测试数据(JDK 17环境下):
-
低竞争场景:
synchronized性能更优 -
高竞争场景:
ReentrantLock(非公平)性能更稳定 -
超高频场景:
synchronized的偏向锁优化效果明显
4.2 选型建议
-
电商秒杀系统:优先选择非公平锁,保证高吞吐量
-
银行转账系统:使用公平锁,确保先来先服务
-
内容审核队列:公平锁避免线程饥饿
5. ReentrantLock vs synchronized:全面对比
5.1 功能特性对比表
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置关键字 | JDK API类 |
| 锁释放 | 自动释放 | 手动unlock() |
| 可中断性 | 不支持 | 支持(lockInterruptibly()) |
| 公平锁 | 仅非公平 | 支持公平/非公平 |
| 超时获取 | 不支持 | 支持(tryLock(timeout)) |
| 条件变量 | 单一队列 | 多Condition支持 |
| 性能 | JDK 1.6后优化良好 | 高并发下更稳定 |
5.2 内存语义一致性
两者都提供相同的内存语义:
-
可见性:
happens-before原则保证 -
原子性:临界区操作不可分割
-
有序性:防止指令重排序
6. 高级特性详解
6.1 可中断锁:优雅处理死锁
public class InterruptibleLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() throws InterruptedException {
lock.lockInterruptibly(); // 可中断的锁获取
try {
// 执行耗时操作
while (!Thread.currentThread().isInterrupted()) {
// 业务逻辑
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
中断流程:false → true → false
-
线程调用
interrupt(),中断标志置为true -
lockInterruptibly()触发InterruptedException -
异常处理后,中断标志被清空(
false)
6.2 超时锁:防止系统雪崩
public class TimeoutLockExample {
private final ReentrantLock lock = new ReentrantLock();
public boolean addData(String item, long timeout, TimeUnit unit)
throws InterruptedException {
if (lock.tryLock(timeout, unit)) { // 尝试获取锁,最多等待timeout
try {
return data.add(item);
} finally {
lock.unlock();
}
} else {
// 超时降级策略
log.warn("获取锁超时,执行异步处理");
asyncService.handleAsync(item);
return false;
}
}
}
超时时间设置原则:
-
参考接口响应时间阈值(如500ms)
-
预留降级处理时间(如300ms超时,200ms降级)
-
根据业务重要性动态调整
6.3 条件变量:精细化线程协作
public class ProducerConsumer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列未满条件
private final Condition notEmpty = lock.newCondition(); // 队列非空条件
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // 等待队列未满
}
queue.put(item);
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
}
相比synchronized的wait()/notify():
-
精准唤醒:只唤醒特定条件的线程
-
多队列管理:不同条件使用不同队列
-
减少无效唤醒:提升并发效率
7. 实战应用场景
7.1 高并发缓存更新(防缓存击穿)
public class CacheService {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private final Map<String, ReentrantLock> keyLocks = new ConcurrentHashMap<>();
public Object get(String key) {
Object value = cache.get(key);
if (value == null) {
ReentrantLock lock = keyLocks.computeIfAbsent(key,
k -> new ReentrantLock());
if (lock.tryLock()) { // 非公平锁,保证吞吐量
try {
// 双重检查
value = cache.get(key);
if (value == null) {
value = loadFromDB(key);
cache.put(key, value);
}
} finally {
lock.unlock();
keyLocks.remove(key);
}
} else {
// 其他线程正在加载,等待
lock.lock();
lock.unlock();
return cache.get(key);
}
}
return value;
}
}
优化要点:
-
锁粒度控制:每个key独立锁,避免全局竞争
-
双重检查:防止重复加载
-
非公平锁:高并发下性能优先
7.2 分布式锁兜底方案
public class DistributedLockWithFallback {
private final ReentrantLock localLock = new ReentrantLock();
private final DistributedLock distributedLock = new RedisDistributedLock();
public boolean deductStock(String productId, int num) {
// 1. 先获取本地锁(300ms超时)
if (!localLock.tryLock(300, TimeUnit.MILLISECONDS)) {
log.warn("本地锁获取失败,请求过于频繁");
return false;
}
try {
// 2. 获取分布式锁(3秒超时)
if (!distributedLock.tryLock(productId, 3, TimeUnit.SECONDS)) {
log.warn("分布式锁获取失败");
return false;
}
try {
// 3. 执行业务逻辑
return doDeductStock(productId, num);
} finally {
distributedLock.unlock(productId);
}
} finally {
localLock.unlock();
}
}
}
释放顺序原则:先释放分布式锁,再释放本地锁
-
防止其他节点误认为资源可用
-
避免跨节点并发问题
8. 最佳实践与避坑指南
8.1 使用规范
8.1.1 必须使用try-finally:
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
8.1.2 避免锁泄漏:
-
锁获取次数必须等于释放次数
-
重入N次必须解锁N次
8.1.3 合理设置超时:
-
根据业务响应时间设置
-
配合降级策略使用
8.2 性能优化
8.2.1 缩小锁粒度:
-
避免大范围同步
-
使用分段锁或读写锁
8.2.2 读多写少场景:
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
8.2.3 虚拟线程注意事项:
-
JDK 21+中,虚拟线程不建议使用
synchronized -
优先使用
ReentrantLock避免线程固定
8.3 常见陷阱
| 陷阱 | 现象 | 解决方案 |
|---|---|---|
| 忘记解锁 | 死锁,线程阻塞 | 必须使用finally块 |
| 重入计数不匹配 | 锁未真正释放 | 确保lock/unlock成对 |
| 滥用公平锁 | 性能下降20%-30% | 非公平锁优先 |
| 锁粒度过大 | 并发度低 | 细粒度锁设计 |
9. 总结
9.1 技术选型决策树
是否需要高级功能?
├── 否 → 优先使用synchronized(简洁安全)
└── 是 → 选择ReentrantLock
├── 需要公平性? → 使用公平锁
├── 需要超时控制? → 使用tryLock(timeout)
├── 需要可中断? → 使用lockInterruptibly()
└── 需要多条件? → 使用Condition
9.2 未来发展趋势
-
虚拟线程普及:
ReentrantLock在虚拟线程中的优势更加明显 -
硬件并发优化:随着CPU核心数增加,锁竞争优化更加重要
-
混合锁策略:根据场景动态选择锁类型将成为趋势
9.3 核心思想
没有最好的锁,只有最适合的锁。ReentrantLock和synchronized各有优劣,关键在于:
-
理解业务场景:根据并发特点选择合适锁
-
掌握底层原理:知其然更要知其所以然
-
持续性能优化:监控、测试、调优闭环
更多推荐


所有评论(0)