aqs的唤醒是严格按fifo顺序的单次链式唤醒:每次释放锁仅调用unparksuccessor唤醒一个有效后继节点,被唤醒线程获取锁后成为新head并触发下一次唤醒,形成“获取-移交-再唤醒”的串行流水线,避免惊群效应。

<p>当并发度瞬间飙升,大量线程争抢同一把锁(如 <code>ReentrantLock</code>),AQS 同步队列会快速堆积挂起线程;而一旦锁被释放,<code>unparkSuccessor</code> 会从头节点开始,**逐个唤醒后继节点**——这不是广播,也不是跳表,而是严格按入队顺序、单向链式、一次一个的“接力唤醒”。下面用可观察的代码演示这一过程。</p> <h3>一、构造可观测的同步队列堆积场景</h3> <p>关键点:让多个线程在 lock() 时必然入队(而非 CAS 抢到 state),并记录入队时刻与节点位置。</p>
- 使用
new ReentrantLock(false)(非公平锁)+ 主动调用lock()触发 AQS 的acquire流程 - 通过反射获取
AbstractQueuedSynchronizer.syncQueue(实际是head字段),遍历链表打印节点状态 - 用
Thread.sleep(1)+ 多线程密集提交,确保竞争激烈、快速入队
二、注入唤醒日志,看清 unparkSuccessor 的链式行为
AQS 原生不打印唤醒细节,需在 unparkSuccessor(Node node) 中插入日志(可通过字节码增强或继承重写方式实现)。核心逻辑如下:
- 只处理传入的
node(即当前释放锁的节点,通常是 head) - 从
node.next开始找第一个ws 的后继节点(跳过已取消节点) - 调用
LockSupport.unpark(s)后,不继续遍历后续节点 —— 这是链式唤醒的关键:本次只唤醒一个 - 被唤醒线程在
acquireQueued中自旋/阻塞后,若成功获取锁,会把自己设为新 head,并在setHead后触发下一轮unparkSuccessor
三、用调试断点 + 线程栈验证“单次只唤醒一个”
在 unparkSuccessor 内部加断点,配合多线程运行,可清晰看到:
- 线程 T1 释放锁 → 触发
unparkSuccessor(head)→ 唤醒 T2(head.next)→ 断点结束 - T2 被唤醒后执行
acquireQueued→ 成功获取锁 →setHead(T2-node)→ 自动调用unparkSuccessor(T2-node) - 此时断点再次命中,但这次唤醒的是 T3(T2.node.next),不是 T4 或 T5
- 唤醒严格遵循 FIFO 链表顺序,无跳跃、无批量、无并发唤醒竞争
四、为什么不是“一口气唤醒所有”?
这是 AQS 的设计哲学:避免惊群(thundering herd)、控制调度开销、保持唤醒与获取的原子性。
- 若一次性 unpark 所有后继,内核需调度大量线程进入就绪态,但多数仍会因锁未释放而立即 park 回去,徒增上下文切换
- 链式唤醒天然形成“获取-移交-再唤醒”的流水线,每个被唤醒线程要么拿锁成功(成为新 head),要么失败继续 park —— 状态收敛稳定
- 闭锁(如
CountDownLatch)底层也复用 AQS,但它的releaseShared是广播式唤醒(doReleaseShared循环 unpark 直到无后继),这和独占锁的链式逻辑有本质区别
不复杂但容易忽略:所谓“链式唤醒”,本质是 每次释放操作只驱动一次 unparkSuccessor,而该方法只唤醒一个后继;下一个唤醒由新 head 的释放动作触发,形成隐式链式传递。代码演示的核心,就是让这个“一次一唤醒”的节奏肉眼可见。










