AQS 源码原理:state、同步队列与 Condition
沿着获取、入队、阻塞、唤醒和条件等待主线,理解 AbstractQueuedSynchronizer。
一、AQS 解决什么问题
AQS 全称 AbstractQueuedSynchronizer,是一套构建锁和同步器的框架。它把同步器的共同机制抽出来:
- 用
state表示同步状态; - 用 FIFO 等待队列管理竞争失败的线程;
- 使用 CAS 修改状态和队列;
- 使用
LockSupport.park/unpark阻塞和唤醒; - 支持独占模式、共享模式和 Condition。
子类主要定义“什么条件下获取或释放状态”,AQS 负责失败后的排队、阻塞和传播。
二、AQS 的核心字段
概念上最重要的是:
private volatile int state;
private transient volatile Node head;
private transient volatile Node tail;
不同 JDK 版本的 Node 字段和状态常量可能调整,但 state + FIFO 同步队列 这条主线稳定。
state 的语义由子类解释
| 同步器 | state 的含义 |
|---|---|
| ReentrantLock | 0 表示空闲,正数表示独占锁重入次数 |
| Semaphore | 剩余许可证数 |
| CountDownLatch | 尚未归零的计数 |
| ReentrantReadWriteLock | 高低位组合记录读写锁状态 |
AQS 不知道 state 具体代表什么,它只提供原子读写和排队框架。
三、模板方法设计
独占模式常由子类实现:
tryAcquire
tryRelease
isHeldExclusively
共享模式常由子类实现:
tryAcquireShared
tryReleaseShared
AQS 的公开/最终方法负责完整流程:
先尝试获取
失败则入队
在合适时机重试
仍失败则 park
释放时唤醒后继
这就是模板方法模式:框架固定流程,子类填入同步策略。
四、独占获取流程
以 ReentrantLock.lock() 为例,概念流程是:
tryAcquire
├─ 成功:设置持有线程并返回
└─ 失败:创建等待节点
↓
追加到队尾
↓
前驱是 head 时再次尝试
↓
仍失败则 park
↓
被唤醒后循环重试
为什么入队后还要循环重试
线程可能因为以下原因醒来:
- 前驱释放锁并
unpark; - 中断;
- 虚假唤醒;
- 超时版本到期。
因此唤醒不等于已经获得锁,必须回到循环重新检查条件。
为什么通常由 head 的直接后继竞争
让队首附近节点优先尝试,可以维持大体 FIFO,减少所有等待线程同时抢锁造成的惊群。
五、AQS 等待队列是什么
AQS 的同步队列常被描述为 CLH 队列的变体。它不是最原始的“所有线程在前驱状态上纯自旋”的 CLH 锁,而是维护显式前后链接,并在需要时阻塞线程。
典型节点包含概念信息:
- 当前等待线程;
- 前驱、后继链接;
- 等待状态;
- 独占或共享模式;
- Condition 队列链接。
head 通常是已经成功获取同步状态的节点或哨兵位置。新节点通过 CAS 竞争 tail 加入队尾。
为什么入队使用 CAS
多个线程可能同时入队,CAS 可以让一个线程成功更新 tail,失败线程重新读取队尾再尝试,避免用一把全局锁保护队列追加。
六、阻塞与唤醒
AQS 使用 LockSupport:
LockSupport.park(this);
LockSupport.unpark(thread);
permit 模型
每个线程最多拥有一个 permit:
unpark可以先于park;- permit 不会无限累积;
- 有 permit 时下一次 park 可以直接返回;
- park 允许虚假返回,所以必须循环检查条件。
相比 wait/notify,LockSupport 不要求调用方先持有某个对象 Monitor,并可精确指定唤醒线程。
七、释放独占锁
ReentrantLock 释放时大致是:
tryRelease减少 state;- state 归零时清除独占持有线程;
- 检查同步队列;
- 唤醒合适的后继节点;
- 被唤醒线程重新执行获取循环。
可重入锁每次成功重入都会增加 state,因此必须对应释放相同次数,state 归零才真正释放所有权。
八、公平锁与非公平锁
非公平获取
新线程可以先直接 CAS 抢 state。即使队列中已有等待线程,它仍可能插队成功。
优点是减少线程切换、提高吞吐;缺点是等待时间方差更大,可能发生饥饿。
公平获取
获取前通常检查:
hasQueuedPredecessors()
如果前面已有等待节点,就不插队,进入或留在队列中。
公平锁只是倾向按等待顺序授予锁,不是严格实时调度保证。
九、共享模式
共享模式允许多个线程同时成功获取状态。tryAcquireShared 的返回值通常表达:
负数:获取失败
零:获取成功,但没有剩余共享能力
正数:获取成功,后继节点还可能继续获取
Semaphore
state 表示许可证数量。获取许可证减少 state,释放增加 state。只要还有许可证,多个线程就能同时通过。
CountDownLatch
state 表示剩余计数。await() 在 state 不为 0 时进入共享等待;countDown() 递减,归零后传播唤醒所有等待线程。
CountDownLatch 一次性使用,归零后不能重置。
ReentrantReadWriteLock.ReadLock
读锁使用共享模式,多个读线程可以同时成功;写锁存在时读获取失败。
十、Condition 的双队列模型
Condition 不是直接在同步队列中等待。调用 await() 的线程先进入 Condition 队列,并完全释放锁;收到 signal() 后,节点被转移到 AQS 同步队列,再重新竞争锁。
await 流程
线程必须持有独占锁
↓
加入 Condition 队列
↓
完全释放当前重入锁
↓
park 等待
↓
被 signal 后进入同步队列
↓
重新获取原来的锁
↓
恢复 await 前的重入次数
↓
await 返回
signal 流程
调用 signal 的线程也必须持有锁。signal 把 Condition 队列中等待最久的有效节点转移到同步队列,并不会把锁直接交给它。
当前 signal 线程退出临界区并释放锁后,被转移节点才有机会获取锁。
十一、中断、取消与超时
等待线程可能因为中断、超时或异常取消。队列必须跳过已取消节点,并保证后继仍有机会被唤醒。
不同 API 的中断语义不同:
lock()获取过程中通常不因中断而抛出,但会保留/恢复中断状态;lockInterruptibly()等待时响应中断并退出;tryLock(timeout)同时处理超时与中断;Condition.await()可响应中断;awaitUninterruptibly()不因中断提前返回。
工程中必须清楚调用的是哪一种语义,不能捕获 InterruptedException 后直接忽略。
十二、AQS 与 ReentrantLock 的关系
ReentrantLock 外层负责 Lock API,内部 Sync 继承 AQS:
ReentrantLock
↓ 委托
Sync extends AQS
├─ NonfairSync
└─ FairSync
state 与 owner
- state 为 0:锁空闲;
- 首次获取:CAS 把 state 从 0 改为 1,并设置独占线程;
- 同线程重入:state 增加;
- unlock:state 减少;
- state 归零:清除 owner,唤醒后继。
只比较 state 不足以实现可重入,还必须判断当前线程是否就是独占持有者。
十三、如何阅读 AQS 源码
不要从所有字段开始背。建议按一条真实调用链:
ReentrantLock.lock();- 公平或非公平
tryAcquire; - AQS 获取失败后的入队;
- park 与 unpark;
unlock()和tryRelease;- 再看共享模式;
- 最后看 Condition 的双队列转移。
阅读时区分三个层次:
同步策略:子类如何解释 state
排队机制:AQS 如何管理失败线程
线程调度:LockSupport 如何 park/unpark
十四、常见误区
- AQS 不是锁,而是构建锁和同步器的框架;
- 入队成功不代表获得锁;
- unpark 不代表线程一定立即运行或获得锁;
- signal 不会让 Condition 线程直接执行;
- 公平锁不是操作系统级严格公平调度;
- Semaphore 控制并发数量,不等同于严格 QPS 限流;
- CountDownLatch 使用共享模式,但它不是可重复屏障;
- AQS 队列是 CLH 思想的工程变体,不是纯自旋 CLH 的原样实现。
十五、高频面试问题
- AQS 的 state 和同步队列分别做什么?
- 子类为什么只需实现 tryAcquire/tryRelease?
- 获取锁失败后如何入队和阻塞?
- 为什么线程被唤醒后还要重新竞争?
- 公平锁与非公平锁在获取路径上有什么区别?
- AQS 独占模式和共享模式有什么区别?
- Condition 为什么需要单独的等待队列?
- signal 后节点如何从 Condition 队列进入同步队列?
- ReentrantLock 如何通过 state 实现重入?
- CountDownLatch 和 Semaphore 如何解释 state?
评论与讨论
回复 :
留下你的想法