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

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

















