Semaphore与CountDownLatch行为差异源于tryAcquireShared返回值语义:Semaphore返回剩余许可数(≥0可继续传播唤醒),CountDownLatch仅在state==0时返回1以批量唤醒;二者state字段语义相反,且共享模式唤醒逻辑由doReleaseShared自旋控制。

为什么 tryAcquireShared 返回值语义决定唤醒行为
关键不在排队逻辑,而在 tryAcquireShared 的返回值怎么用:≥0 才算成功,且该数值会直接影响后续是否传播唤醒。
Semaphore 的 tryAcquireShared 返回的是剩余许可数(available - acquires),比如 state=3、acquire(1),就返回 2;而 CountDownLatch 的实现里,只要 state == 0 就返回 1,否则一律返回 -1。
-
Semaphore:返回值 > 0 表示“还有富余”,后续setHeadAndPropagate会继续尝试唤醒后继共享节点 -
CountDownLatch:只在 state == 0 时返回 ≥0(固定为 1),意味着“条件达成”,于是批量唤醒所有等待者;一旦唤醒完成,state 永远卡在 0,后续await()直接跳过排队 - 常见错误:重写
tryAcquireShared时用return true/false编译能过,但 AQS 会把true当作 1、false当作 0,导致唤醒逻辑彻底错乱
releaseShared 怎么区分“唤醒一个”和“唤醒全部”
release(int)(独占)只调一次 unparkSuccessor(head);而 releaseShared(int) 必须进 doReleaseShared() 自旋块——这才是共享模式的真正分水岭。
这个自旋块干两件事:一是 CAS 修改 head 的 waitStatus 确保传播可达;二是只要当前 state 仍满足条件(如 Semaphore 剩余许可 > 0,或 CountDownLatch 已归零),就持续唤醒后继节点。
-
Semaphore.release():state 加回许可数后,doReleaseShared()可能只唤醒一个等待线程,也可能唤醒多个(取决于当时有多少线程在排队 + 剩余许可数) -
CountDownLatch.countDown():每次只减 1,直到最后一次把 state 减到 0;此时doReleaseShared()会一路唤醒所有排队节点,且不依赖 state 再次变化 - 容易踩的坑:
CountDownLatch的tryReleaseShared必须用无符号右移 + CAS 循环校验 state 是否真为 0,否则可能漏唤醒
state 字段的用途差异直接暴露设计意图
两者都用 AQS 的 state 字段,但语义完全相反:
-
Semaphore:state 是“可用许可数”,初始值即最大并发数;acquire 消耗它,release 补充它;可反复增减 -
CountDownLatch:state 是“待完成任务数”,初始值即倒计时起点;countDown 单向递减;归零即终态,不可逆 - 性能影响:Semaphore 每次 acquire/release 都要 CAS 更新 state 并判断剩余值;CountDownLatch 在归零后,所有 await() 调用直接返回,几乎零开销
- 注意:
CountDownLatch的await()不修改 state,只是读;而Semaphore.acquire()必须 CAS 修改 state,失败才入队
公平性开关对共享模式的实际影响很有限
Semaphore 支持公平/非公平构造,CountDownLatch 不支持——但这不是 API 设计疏忽,而是语义使然。
Semaphore 的公平性体现在 tryAcquireShared 开头加了 hasQueuedPredecessors() 判断,阻止新线程插队;但 CountDownLatch 的 await() 本质是“等别人干活”,谁先来等、谁后等并不影响结果,加公平性没意义。
- 实操建议:高并发许可争抢场景(如连接池),优先用非公平
Semaphore,减少 CAS 自旋开销 - 陷阱:别试图给
CountDownLatch加锁或同步包装来模拟公平等待,它本身就不承诺等待顺序 - 真正要注意的其实是:
CountDownLatch无法重置,要用CyclicBarrier替代重复场景
AQS 共享模式的骨架一致,但 tryAcquireShared 和 tryReleaseShared 的实现细节,才是 Semaphore 和 CountDownLatch 行为差异的全部来源。绕开源码看文档,很容易把“都能阻塞线程”当成共性,却忽略一个在循环争抢资源、另一个在单向等待事件的本质区别。

















