ReentrantLock JDK 17 源码剖析:从 AQS 队列到公平/非公平锁的 3 种加锁路径
ReentrantLock JDK 17 源码深度解析:AQS 队列与三种加锁路径的全景剖析
引言:为什么需要深入理解 ReentrantLock?
在 Java 并发编程领域,ReentrantLock作为synchronized关键字的替代方案,提供了更灵活的锁控制能力。但真正让开发者困惑的往往不是它的 API 调用,而是其底层实现机制。本文将基于 JDK 17 最新源码,从 AQS 队列运作机制到三种不同的加锁路径(快速 CAS、重入锁、队列等待),为你揭示这个并发利器的核心设计思想。
与市面上大多数基于 JDK 8 的分析不同,JDK 17 中的ReentrantLock在自旋策略、节点状态管理等方面进行了多项优化。理解这些变化不仅能帮助你在面试中脱颖而出,更能让你在实际高并发场景中做出更合理的技术选型。
1. AQS 架构与 ReentrantLock 的关系
1.1 AQS 的双队列模型
AbstractQueuedSynchronizer(AQS)是ReentrantLock的基石,它维护了两个核心队列:
- 同步队列:双向链表结构,存储等待获取锁的线程
- 条件队列:单向链表,存储等待特定条件的线程(与
Condition接口相关)
// AQS 中的节点定义(JDK 17 精简版) static final class Node { volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 关联线程 volatile int waitStatus; // 等待状态 Node nextWaiter; // 条件队列专用 }1.2 ReentrantLock 的类结构
JDK 17 中ReentrantLock的继承关系如下:
ReentrantLock ├── Sync (extends AbstractQueuedSynchronizer) │ ├── NonfairSync │ └── FairSync关键点在于:
Sync是抽象内部类,继承自 AQSNonfairSync实现非公平锁逻辑FairSync实现公平锁逻辑
2. 三种加锁路径的源码解析
2.1 路径一:快速 CAS 抢占(非公平锁专属)
非公平锁在加锁时会首先尝试快速抢占:
// NonfairSync.lock() final void lock() { if (compareAndSetState(0, 1)) // 无锁状态直接抢占 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }执行流程:
- 通过 CAS 尝试将 state 从 0 改为 1
- 成功则设置当前线程为独占线程
- 失败则进入常规获取流程
注意:JDK 17 优化了
compareAndSetState的内存语义,使用VarHandle替代了原来的Unsafe操作
2.2 路径二:重入锁实现
当线程已经持有锁时,会进入重入逻辑:
// Sync.nonfairTryAcquire() final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 重入判断 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); // state 累加 return true; } return false; }重入特性:
- 通过
getExclusiveOwnerThread()判断当前线程是否已持有锁 - state 值记录重入次数
- 必须等所有重入锁释放(state=0)才会真正释放锁
2.3 路径三:入队等待
当锁被其他线程持有时,当前线程会进入队列等待:
// AbstractQueuedSynchronizer.acquire() public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }关键步骤:
addWaiter():将线程包装为 Node 加入队列尾部acquireQueued():在队列中等待获取锁- JDK 17 优化了入队操作的内存屏障使用
队列状态流转示例:
| 状态 | 值 | 含义 |
|---|---|---|
| CANCELLED | 1 | 线程已取消 |
| SIGNAL | -1 | 后继线程需要唤醒 |
| CONDITION | -2 | 在条件队列中等待 |
| PROPAGATE | -3 | 共享模式下传播唤醒 |
3. 公平锁 vs 非公平锁的差异
3.1 公平锁的核心判断
公平锁在尝试获取锁时会检查队列:
// FairSync.tryAcquire() 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; } } // ... 重入逻辑与非公平锁相同 }3.2 hasQueuedPredecessors() 方法解析
这是公平性的核心实现:
public final boolean hasQueuedPredecessors() { Node h = head, t = tail; Node s; return h != t && // 队列不为空 ((s = h.next) == null || s.thread != Thread.currentThread()); }判断逻辑:
- 如果队列为空或当前线程已在队首,可以尝试获取锁
- 否则必须排队
3.3 性能对比(JDK 17 优化)
| 指标 | 公平锁 | 非公平锁 |
|---|---|---|
| 吞吐量 | 较低 | 高 20%~30% |
| 饥饿可能性 | 无 | 可能发生 |
| 上下文切换 | 较多 | 较少 |
| JDK 17 优化 | 减少不必要的唤醒 | 优化 CAS 重试策略 |
4. JDK 17 的重要优化点
4.1 自旋策略改进
JDK 17 引入了更智能的自旋等待:
// AbstractQueuedSynchronizer.acquire() if (first && spins != 0) { --spins; Thread.onSpinWait(); // JVM 提示当前在自旋 }优化效果:
- 减少不必要的线程挂起
- 在短时间锁持有场景提升性能
4.2 内存访问优化
使用VarHandle替代Unsafe:
// AbstractQueuedSynchronizer 中的定义 private static final VarHandle STATE; private static final VarHandle HEAD; private static final VarHandle TAIL; static { try { MethodHandles.Lookup l = MethodHandles.privateLookupIn( AbstractQueuedSynchronizer.class, MethodHandles.lookup()); STATE = l.findVarHandle(AbstractQueuedSynchronizer.class, "state", int.class); // ... 其他初始化 } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } }优势:
- 更好的类型安全
- 更精确的内存排序控制
4.3 取消节点处理优化
JDK 17 改进了取消节点的清理逻辑:
// 新版 cleanQueue() 实现 private void cleanQueue() { for (;;) { Node h = head; if (h == null || h.next == null) return; // ... 更高效的遍历逻辑 } }5. 实战:如何选择锁策略
5.1 选择依据
| 场景 | 推荐策略 |
|---|---|
| 锁持有时间短 | 非公平锁 |
| 要求严格顺序 | 公平锁 |
| 高竞争环境 | 非公平锁 + 自旋优化 |
| 防止饥饿 | 公平锁 |
5.2 最佳实践示例
// 高性能计数器实现 class Counter { private final ReentrantLock lock = new ReentrantLock(false); // 非公平锁 private int count; public void increment() { lock.lock(); // 1. 快速路径尝试 try { count++; } finally { lock.unlock(); } } // 带超时的安全获取 public boolean safeIncrement(long timeout, TimeUnit unit) throws InterruptedException { if (lock.tryLock(timeout, unit)) { try { count++; return true; } finally { lock.unlock(); } } return false; } }6. 常见问题排查技巧
6.1 死锁检测
使用ThreadMXBean检测:
ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); if (threadIds != null) { ThreadInfo[] infos = bean.getThreadInfo(threadIds); for (ThreadInfo info : infos) { System.out.println(info.getLockName()); } }6.2 锁竞争分析
通过 JFR 监控:
jcmd <pid> JFR.start duration=60s filename=lock.jfr分析事件:
jdk.JavaMonitorWaitjdk.ThreadPark
7. 从源码看设计哲学
- 分层设计:AQS 提供骨架,子类实现特定语义
- 乐观并发:CAS 优先于阻塞
- 渐进式回退:从快速路径到完全排队
- 状态压缩:用单个
state变量管理复杂状态
在 JDK 17 的实践中,这些设计原则得到了进一步强化,特别是在:
- 减少内存屏障使用
- 优化热点路径
- 改进取消机制
理解这些底层机制,才能真正掌握 Java 并发编程的精髓。