从 JDK 21 源码理解 AQS 与 ReentrantLock
ReentrantLock 把“谁能获取锁”的规则交给内部 Sync,AQS 负责竞争失败后的排队、阻塞和重试。阅读时应把资源状态、同步队列、条件队列分开:队列记录等待者,真正决定互斥的是状态转换和所有者检查。
state:锁的重入次数
本文以 OpenJDK JDK 21 的 ReentrantLock 源码为依据,重点看 Sync、NonfairSync、FairSync,不要直接套用旧版本的方法结构。
AQS 的 state 是 volatile int,含义由子类定义。在这把锁中,0 表示空闲,正数表示当前所有者的持有次数;线程身份另存在 exclusiveOwnerThread 中。不能把 state 理解成等待线程数,也不能只凭一个正数判断当前线程持锁。
首次获取用 CAS 将 0 改为 1,成功后登记所有者;同一线程再次获取只增加计数,因为其他线程没有修改其持有次数的资格。比如外层方法持锁后调用同样加锁的内层方法,状态经历 0 → 1 → 2 → 1 → 0。内层退出只还回一次持有权,锁仍由外层持有。
独占获取与释放:唤醒后仍要竞争
JDK 21 的入口是 Sync.lock():先执行 initialTryLock(),失败才调用 acquire(1)。非公平版本先直接 CAS;公平版本先检查队列,二者都允许所有者重入。随后 AQS 调用子类 tryAcquire,失败者进入同步队列,在循环中检查前驱、再次尝试、设置等待状态并阻塞。它不是一次 CAS 失败就无条件睡眠。
按 JDK 21 AQS 源码阅读 acquire、release、signalNext,可以看到:解锁唤醒后继只是让它有机会继续获取,不会替它设置所有者。非公平竞争者可能在这段时间先拿走锁,醒来的线程仍须检查条件并重试。
unlock() 委托 release(1),tryRelease 先核验所有者,再减去持有次数;只有减到 0 才清空所有者并返回完全释放成功,AQS 才通知后继。提前返回、业务异常都应通过 finally 配对解锁。未持锁者解锁会抛出 IllegalMonitorStateException。
普通 lock() 不因等待期间中断而退出;业务需要取消等待时,应选择 lockInterruptibly() 或带超时的 tryLock,并按接口处理中断,不能认为队列里所有等待都可取消。
Condition:从条件队列回到同步队列
每次 newCondition() 创建独立条件队列,同一把锁仍只有一个同步队列。依据 AQS 官方契约,条件等待需要支持保存完整 state、完全释放以及重新获取后恢复 state。
在正常等待路径中,ConditionObject.enableWait 将节点加入条件队列,保存 state,再 release(savedState)。因此重入两次后 await 会释放两次持有权,否则生产者永远无法拿锁修改条件。signal 把有效等待节点转入同步队列;发送通知的线程仍持锁,等待者必须重新获取锁并恢复原计数,才能继续执行。
必须在持锁时检查业务条件,用 while 包住 await。Condition 官方文档允许虚假唤醒;即使收到有效通知,重新获锁前资源也可能已被其他消费者取走。通知本身不保存业务事实,队列为空时 signal 不会为未来等待者储存许可;真正要保存的是受同一把锁保护的共享状态。
公平性:排队规则取决于入口
ReentrantLock 官方 API强调,公平锁偏向等待最久的线程,但不保证操作系统公平调度,也可能牺牲吞吐量。
| 入口 | 公平锁上的行为 |
|---|---|
lock() 初次尝试 | initialTryLock 检查 hasQueuedThreads() |
排队后的 tryAcquire | 用 hasQueuedPredecessors() 防止越过前驱 |
| 所有者再次获取 | 允许重入,不排到自己后面 |
无参 tryLock() | 忽略公平设置,空闲时可以插队 |
tryLock(0, TimeUnit.SECONDS) | 遵守公平设置,同时检测中断 |
所以 FIFO 队列不等于所有调用都严格 FIFO。观察几次线程打印顺序也无法证明公平性;应先确认构造参数与实际使用的获取方法。
可运行示例:观察两次持有权的释放与恢复
完整源码见仓库 examples/java-advanced/aqs-reentrantlock/LockDemo.java。核心逻辑如下,ready 的读写均在同一把锁内:
lock.lock();
try {
while (!ready) changed.await();
consume();
} finally {
lock.unlock();
}
// 通知者在持锁区内:ready = true; changed.signal();上面是业务结构示意,完整程序额外重入一次,并以 awaitNanos 设置等待期限。门闩协调启动后,主线程必须获取同一把锁才能发通知,从而验证等待者确实完全释放了锁。程序还检查条件队列、恢复计数以及最终空闲状态,工作线程异常会传给主线程使运行失败。
本机实际使用 Oracle JDK 21.0.12 编译运行:
javac --release 21 -encoding UTF-8 -d docs/upgrade-100/checks/aqs-reentrantlock/classes examples/java-advanced/aqs-reentrantlock/LockDemo.java
java -ea -cp docs/upgrade-100/checks/aqs-reentrantlock/classes LockDemo输出依次包含 before await: holds=2、通知者仍持锁、after await: holds=2、部分解锁后 1、最终 locked=false,最后为 PASS。三个预期失败样例分别是不持锁调用 unlock、signal、await;它们实际抛出并被检查为 IllegalMonitorStateException,不是测试失败。命令、版本、完整输出见 docs/upgrade-100/checks/aqs-reentrantlock/run.txt,复现脚本为同目录 run.ps1。这次运行验证 API 行为,不是吞吐量测试,也没有通过反射读取内部 state。
继续阅读
可结合已有的乐观锁与悲观锁理解 CAS 与阻塞的关系,再读线程池的队列与过载边界区分任务排队和锁竞争排队。本文源码与契约依据均为上述 OpenJDK、Oracle 官方资料。
