引言
在多线程编程中,锁机制是确保线程安全的核心工具之一。Java 提供了两种锁机制:隐式锁 synchronized
和显式锁 ReentrantLock
。ReentrantLock
以其灵活性、高性能和丰富的功能,成为复杂并发场景的首选工具。
本文将从设计思想、底层实现、常见问题到最佳实践,深入解析 ReentrantLock
的工作原理,并揭示其依赖的核心框架 AQS(AbstractQueuedSynchronizer) 。
一、设计思想:为什么选择 ReentrantLock?
1. 显式锁 vs 隐式锁
-
synchronized
的局限性:- 锁的获取与释放由 JVM 隐式管理,无法灵活控制(如超时、中断)。
- 仅支持单一条件变量(通过
wait()
和notify()
),难以实现多条件等待(如生产者-消费者模型)。
-
ReentrantLock
的优势:-
开发者手动调用
lock()
和unlock()
,结合try-finally
确保锁释放。 -
提供更细粒度的控制能力:
- 可中断 :通过
lockInterruptibly()
响应中断。 - 超时机制 :通过
tryLock(5, TimeUnit.SECONDS)
避免死锁。 - 公平性策略:支持按线程请求顺序分配锁(减少饥饿问题)。
- 可中断 :通过
-
2. 可重入性(Reentrancy)
-
问题场景:
javapublic void recursiveMethod() { lock.lock(); try { if (condition) recursiveMethod(); // 递归调用 } finally { lock.unlock(); } }
若锁不可重入,递归调用会导致线程死锁(自身等待自身释放锁)。
-
实现原理:
- 通过
state
字段记录锁的重入次数。 - 每次重入时
state
递增,释放时递减,归零后完全释放锁。
- 通过
3. 公平性与性能的权衡
-
公平锁:
- 按线程请求顺序分配锁,避免饥饿。
- 缺点:增加上下文切换,吞吐量较低。
-
非公平锁(默认) :
- 允许新请求的线程"插队"直接尝试获取锁。
- 优点:减少线程挂起/唤醒的开销,吞吐量更高。
-
如何选择:
- 默认使用非公平锁,仅在严格要求顺序时选择公平锁(如交易系统)。
4. 灵活性扩展
-
Condition
条件变量:- 替代
Object.wait()
/notify()
,支持多个等待队列。 - 典型场景:生产者-消费者模型中分离"非满"和"非空"条件。
javaReentrantLock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition();
- 替代
二、底层原理:AQS 的核心机制
1. AQS 的设计思想
AQS 是 ReentrantLock
的基石,其核心思想是通过 模板方法模式 分离通用逻辑(如线程排队)和特定逻辑(如锁获取规则)。
-
资源状态抽象:
state
字段:volatile int
类型,表示资源状态(如锁的重入次数)。
-
线程排队机制:
- CLH 变体队列:双向链表管理等待线程,节点保存线程引用和状态。
- 虚拟头节点:简化队列操作,头节点不关联实际线程。
2. 加锁流程(非公平锁为例)
java
final void lock() {
if (compareAndSetState(0, 1)) // 尝试直接抢锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入 AQS 排队逻辑
}
-
步骤详解:
-
快速抢锁 :通过 CAS 尝试修改
state
为 1。 -
抢锁成功:标记当前线程为锁持有者。
-
抢锁失败 :调用
acquire(1)
,进入队列排队。tryAcquire()
:再次尝试抢锁(非公平锁允许插队)。- 入队后调用
LockSupport.park()
挂起线程。
-
3. 解锁流程
java
public void unlock() {
sync.release(1);
}
-
步骤详解:
- 调用
tryRelease()
减少state
值,若归零则清除持有线程标记。 - 唤醒队列中下一个未取消的线程(通过
unparkSuccessor()
)。
- 调用
4. 公平锁的特殊处理
java
protected final boolean tryAcquire(int acquires) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
// 重入逻辑...
}
hasQueuedPredecessors()
:检查队列中是否有其他线程等待,确保按顺序分配锁。
三、常见问题与最佳实践
1. ReentrantLock vs synchronized
特性 | ReentrantLock | synchronized |
---|---|---|
锁获取方式 | 显式(lock() /unlock() ) |
隐式(代码块/方法) |
可中断 | 支持(lockInterruptibly() ) |
不支持 |
超时机制 | 支持(tryLock() ) |
不支持 |
公平性 | 支持(构造函数参数) | 不支持 |
条件变量 | 多条件(Condition ) |
单一条件(wait() /notify() ) |
2. 为什么非公平锁性能更高?
-
减少上下文切换:新请求的线程可直接尝试抢锁,无需排队。
-
示例场景:
- 线程 A 释放锁,唤醒线程 B;此时线程 C 请求锁,可能直接抢到锁,而线程 B 仍需唤醒。
3. 内存可见性保证
state
字段由volatile
修饰,确保多线程间的可见性。- CAS 操作(通过
Unsafe
类)保证原子性。
4. 最佳实践
-
始终在
finally
中释放锁:javaReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }
-
避免锁嵌套:按固定顺序获取多个锁,防止死锁。
-
优先使用
tryLock
:设定超时时间,避免无限等待。
四、总结
ReentrantLock
通过 AQS 实现了一套高度灵活的锁机制,其核心优势在于:
- 功能丰富:支持可中断、超时、公平锁、多条件变量。
- 性能优异:非公平锁设计最大化吞吐量。
- 可扩展性:结合 AQS 可快速实现复杂同步工具。
理解 ReentrantLock
的关键:
- 掌握 AQS 的
state
管理、CLH 队列和模板方法模式。 - 根据场景选择公平性策略,合理使用
Condition
分离等待条件。 - 遵循最佳实践,避免锁泄漏和死锁。