CountDownLatch唤醒多个线程是因为其基于AQS共享模式,state归零后触发setHeadAndPropagate,利用PROPAGATE状态(值-3)主动传播唤醒信号,持续调用unparkSuccessor唤醒后续SHARED节点,实现批量唤醒而非单次SIGNAL式唤醒。

CountDownLatch 唤醒时为什么不是只唤醒一个线程?
因为 CountDownLatch 是共享模式同步器,它的 await() 调用者全部进入 AQS 队列等待,而释放逻辑(countDown())触发的是 releaseShared,最终走到 setHeadAndPropagate —— 这个方法才是批量唤醒的开关。
关键点在于:当 state 减到 0 的那一刻,AQS 不是简单地 unpark 一个节点,而是检查当前 head 后继是否也为共享节点(node.nextWaiter == Node.SHARED),并结合 PROPAGATE 状态持续向下传播唤醒信号。
-
PROPAGATE状态不会由用户设置,它只在setHeadAndPropagate中被写入,表示“我已获取成功,后续共享节点也该被唤醒” - 如果队列中连续多个节点都是
SHARED(即都调用了await()),PROPAGATE就会像多米诺骨牌一样逐个触发unparkSuccessor - 哪怕中间某个节点因中断或超时已出队,传播逻辑仍会跳过它继续找下一个有效的共享节点
PROPAGATE 状态怎么出现在节点上?
它不靠初始化,也不靠 CAS 直接设,而是在每次共享获取成功后,由 setHeadAndPropagate(node, propagate) 主动写入新 head 的 waitStatus 字段。这个动作发生在:
-
CountDownLatch.tryReleaseShared返回true(即state刚减为 0)之后 - AQS 执行
doReleaseShared,先 unpark 第一个等待节点,再调用setHeadAndPropagate - 此时传入的
propagate参数通常 > 0(比如CountDownLatch里固定传 1),满足写入PROPAGATE的条件
注意:PROPAGATE 只写在新 head 上,不是每个节点都有;它的存在就是为了告诉后续线程:“别等我唤醒你了,我已把唤醒权交出去了”。
为什么不用 SIGNAL 就能批量唤醒?
SIGNAL 是独占模式专用状态,语义是“请唤醒我的后继节点”,但它只触发一次 unparkSuccessor,且后继一旦被唤醒、获取成功,就不会再主动传播。而 PROPAGATE 的设计目标就是打破“单次唤醒”限制:
-
SIGNAL节点被唤醒后,执行acquireQueued→ 成功获取 → 设置自己为 head → 停止传播 -
PROPAGATE节点被唤醒后,执行doAcquireShared→ 成功获取 → 调用setHeadAndPropagate→ 再次尝试唤醒后继 - 只要后继仍是
SHARED节点,且 head 的waitStatus == PROPAGATE或propagate > 0,传播就不停止
这就是为什么 10 个线程同时 await(),countDown() 一次归零后,它们几乎同时恢复执行 —— 不是并发调度的巧合,而是 PROPAGATE 驱动的确定性传播链。
实际调试时怎么看 PROPAGATE 是否生效?
在断点停在 setHeadAndPropagate 时,观察两个关键值:
-
h.waitStatus:刚设为 head 的节点,其waitStatus应为Node.PROPAGATE(值为 -3) s == null || s.waitStatus :这个判断决定是否立即唤醒后继;若s存在且s.waitStatus == 0(初始状态)或PROPAGATE,就会触发unparkSuccessor
最容易忽略的是:传播不是“一次性广播”,而是“逐跳接力”。如果某次 unparkSuccessor 唤醒的节点还没来得及完成 setHeadAndPropagate,下一轮传播就会卡住 —— 这也是为什么高竞争下少量线程延迟唤醒是正常现象,不是 bug。











