1. ReentrantLock 的设计背景与核心价值
在 Java 并发编程领域,锁机制是协调多线程访问共享资源的基础工具。synchronized 作为 Java 内置的关键字虽然简单易用,但在复杂场景下存在诸多限制。2004 年 JSR-166 引入的 java.util.concurrent 包中,ReentrantLock 作为其核心组件之一,提供了比 synchronized 更丰富的功能集。
关键设计考量:ReentrantLock 的诞生并非为了替代 synchronized,而是为开发者提供更细粒度的控制能力。其名称中的"Reentrant"(可重入)直接表明了它的核心特性——持有锁的线程可以重复获取同一把锁而不会造成死锁。
从架构层面看,ReentrantLock 采用了经典的模板方法模式,将锁的核心逻辑委托给内部同步器 Sync 实现。这种设计带来三个显著优势:
- 职责分离:锁的公共接口与具体实现解耦
- 扩展灵活:支持公平/非公平两种策略
- 性能优化:通过 AQS 的 CLH 队列减少竞争
2. AQS 深度解析:ReentrantLock 的基石
2.1 AQS 核心机制
AbstractQueuedSynchronizer(AQS)是 JUC 包的灵魂组件,其设计精髓在于:
- 通过 volatile int state 表示同步状态
- 内置 FIFO 等待队列管理阻塞线程
- 提供模板方法供子类实现特定同步语义
对于 ReentrantLock 而言,state 的语义非常直观:
- 0 表示锁未被任何线程持有
- 1 表示锁被独占
-
1 表示锁的重入次数
java复制// AQS 中的关键字段
private transient volatile Node head; // 队列头节点
private transient volatile Node tail; // 队列尾节点
private volatile int state; // 同步状态
2.2 CLH 队列变种实现
AQS 的等待队列是 CLH 锁的变体,其节点结构包含以下关键字段:
java复制static final class Node {
volatile int waitStatus; // 等待状态(CANCELLED/SIGNAL/CONDITION/PROPAGATE)
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待线程
Node nextWaiter; // 条件队列链接
}
队列管理采用"懒加载"策略:
- 新节点通过 CAS 入队
- 头节点仅作为哨兵,不关联实际线程
- 取消节点会触发清理操作
3. 锁获取流程全解析
3.1 非公平锁获取流程
以默认的非公平模式为例,lock() 的完整调用链如下:
java复制// ReentrantLock.java
public void lock() {
sync.acquire(1); // 入口方法
}
// AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试快速获取
acquireQueued( // 加入队列等待
addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
关键步骤拆解:
- initialTryLock():直接 CAS 尝试获取锁(非公平性的体现)
- 获取失败后,通过 addWaiter() 创建独占模式节点并入队
- acquireQueued() 中自旋尝试获取锁,必要时阻塞
java复制// NonfairSync 的实现
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) { // 关键CAS操作
setExclusiveOwnerThread(current);
return true;
}
return false;
}
3.2 公平锁获取差异
公平锁的 tryAcquire 实现增加了队列检查:
java复制protected final boolean tryAcquire(int acquires) {
if (!hasQueuedPredecessors() && // 关键差异点
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
// ... 重入逻辑相同
}
hasQueuedPredecessors() 方法会检查:
- 队列是否为空
- 当前线程是否是队列头节点的后继线程
4. 锁释放机制剖析
unlock() 操作触发以下关键流程:
java复制public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继
return true;
}
return false;
}
tryRelease 的核心逻辑:
- 检查当前线程是否是锁持有者
- state 减 1 后若为 0,则完全释放
- 仅当 state 归零时才返回 true
java复制protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 完全释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 注意:此处不需要CAS
return free;
}
5. 可重入性实现细节
ReentrantLock 的可重入特性体现在:
java复制// 非公平锁中的重入处理
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
关键设计要点:
- 通过 exclusiveOwnerThread 记录持有线程
- 每次重入 state 递增
- 释放时需对应次数的 unlock 调用
- 最大重入次数为 Integer.MAX_VALUE
典型使用模式:
java复制lock.lock();
try {
// 临界区1
lock.lock(); // 重入
try {
// 临界区2
} finally {
lock.unlock();
}
} finally {
lock.unlock(); // 必须匹配释放
}
6. 条件变量实现原理
Condition 接口的实现是 ReentrantLock 的另一个亮点:
java复制public Condition newCondition() {
return sync.newConditionObject();
}
// AQS中的实现
final ConditionObject newConditionObject() {
return new ConditionObject();
}
6.1 等待队列管理
每个 ConditionObject 维护独立的条件队列:
- 节点类型为 Node.CONDITION
- 单向链表结构
- 与同步队列共享节点类
await() 的核心操作:
- 释放锁
- 创建 CONDITION 节点加入条件队列
- 阻塞直到被 signal 或中断
java复制public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); // 加入条件队列
int savedState = fullyRelease(node); // 完全释放锁
// ... 阻塞和唤醒处理
}
6.2 信号传递机制
signal() 操作将节点从条件队列转移到同步队列:
java复制public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); // 转移第一个等待者
}
转移过程包含:
- 修改节点状态从 CONDITION 到 0
- 通过 enq() 方法加入同步队列
- 设置前驱节点的 waitStatus 为 SIGNAL
7. 性能优化策略
7.1 非公平锁的性能优势
非公平锁的吞吐量优势来自:
- 新请求线程可以直接插队尝试获取锁
- 减少线程挂起/唤醒的开销
- 利用线程调度的时间窗口
测试数据显示:
- 高竞争场景下,非公平锁吞吐量可提升 20-30%
- 但可能造成线程饥饿现象
7.2 自旋优化
在 acquireQueued 方法中:
java复制final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) { // 自旋
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt(); // 最终阻塞
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
优化点包括:
- 检查前驱节点是否为 head(最佳候选者)
- 多次尝试失败后才真正阻塞
- 通过 shouldParkAfterFailedAcquire 设置 SIGNAL 状态
8. 最佳实践与陷阱规避
8.1 正确使用范式
推荐代码结构:
java复制ReentrantLock lock = new ReentrantLock();
// ...
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须放在finally块
}
常见错误:
- 忘记调用 unlock() 导致死锁
- 在 lock() 和 try 之间插入可能抛出异常的代码
- 重入锁释放次数不匹配
8.2 性能调优建议
根据场景选择策略:
- 低竞争:非公平锁(默认)
- 严格公平性要求:公平锁
- 短临界区:尝试 tryLock 带超时
监控指标:
- 锁持有时间
- 等待队列长度
- 竞争失败率
9. 与 synchronized 的深度对比
9.1 实现层面差异
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现机制 | Java 代码 + AQS | JVM 内置指令 |
| 锁获取方式 | 显式 lock()/unlock() | 隐式 monitorenter/exit |
| 中断响应 | 支持 lockInterruptibly() | 不支持 |
| 条件变量 | 多 Condition 支持 | 单一 wait/notify |
9.2 适用场景选择
优先使用 synchronized 当:
- 简单的同步需求
- 锁获取模式固定
- 不需要高级特性
选择 ReentrantLock 当:
- 需要可中断的锁获取
- 需要尝试获取锁(tryLock)
- 需要公平性控制
- 需要多个条件变量
10. 源码级调试技巧
10.1 关键断点设置
调试 ReentrantLock 时建议监控:
- AQS.state 字段变化
- exclusiveOwnerThread 变更
- 等待队列的头尾节点变化
10.2 诊断工具推荐
- jstack:查看线程锁持有情况
- JConsole:监控锁竞争
- Arthas:运行时分析锁状态
示例诊断命令:
bash复制# 查看锁信息
jstack <pid> | grep -A 10 'java.util.concurrent.locks'
11. 扩展应用场景
11.1 读写锁实现
ReentrantReadWriteLock 基于相似的 AQS 机制:
- 高 16 位表示读锁计数
- 低 16 位表示写锁计数
11.2 分布式锁适配
通过扩展 AQS 可以实现:
- 基于 Redis 的分布式锁
- Zookeeper 互斥锁
- 数据库乐观锁
12. 常见问题排查
12.1 死锁诊断
典型症状:
- 线程 Dump 显示多个线程互相等待
- CPU 使用率低但程序无进展
排查方法:
- 检查锁获取顺序是否一致
- 确认 unlock 是否都被执行
- 使用 tryLock 设置超时
12.2 性能瓶颈
优化方向:
- 减小临界区范围
- 降低锁粒度
- 考虑读写锁分离
13. 版本演进与优化
Java 各版本中的重要改进:
- Java 6:优化了 synchronized 性能
- Java 8:改进 AQS 实现
- Java 9:新增 onSpinWait 提示
14. 替代方案比较
14.1 StampedLock 对比
优势:
- 乐观读模式
- 更好的读多写少性能
局限:
- 不可重入
- 更复杂的API
14.2 并发容器选择
根据场景考虑:
- ConcurrentHashMap
- CopyOnWriteArrayList
- LinkedBlockingQueue
15. 设计模式应用
ReentrantLock 体现了:
- 模板方法模式(AQS)
- 策略模式(公平/非公平)
- 工厂方法模式(Condition 创建)
16. 内存可见性保证
锁提供的 happens-before 保证:
- unlock 操作前的写对后续 lock 可见
- 类似 synchronized 的内存语义
17. 线程转储分析
关键信息解读:
- "waiting on condition" 表示线程阻塞
- "locked <0x000000076bf62208>" 显示锁持有
- "parking to wait for <0x000000076bf62208>" 表示等待
18. 锁分段技术
ConcurrentHashMap 的优化思路:
- 将数据分段
- 每段独立加锁
- 提高并发度
19. 锁消除优化
JIT 编译器在以下情况会消除锁:
- 对象是线程局部的
- 不可能存在竞争
20. 未来发展方向
Java 并发编程趋势:
- 更细粒度的并发控制
- 硬件感知的同步优化
- 异步编程模型整合