countdownlatch 和 semaphore 的共性在于 aqs 共享模式的三处核心契约:state 表示剩余资源数、tryacquireshared 返回值决定是否传播唤醒、doreleaseshared 实现自旋式条件唤醒;二者均复用 aqs 的 cas/volatile 机制,围绕整型计数实现多线程并发判断与协作。

CountDownLatch 和 Semaphore 的共性不在 API 表面,而在 AQS 共享模式的三处核心契约上:状态语义、tryAcquireShared 返回值约定、以及 doReleaseShared 中的传播唤醒逻辑。
CountDownLatch 和 Semaphore 都用 getState() 表示“剩余资源数”
它们都把 AQS 内部的 state 字段直接映射为业务含义明确的计数:
-
CountDownLatch:初始化时state = count,每次countDown()调用tryReleaseShared将state原子减 1;await()实际是循环调用acquireShared(1),直到state == 0 -
Semaphore:初始化时state = permits,acquire()尝试将state减去请求许可数(如 1),release()则加回;state始终代表“当前可用许可数”
关键点在于:两者都不重定义 state 的增减逻辑,而是复用 AQS 提供的 CAS + volatile 保障,且全部操作围绕一个整型计数展开——这正是共享模式“多线程可同时成功”的前提:资源是否充足,只看这个数还剩多少。
tryAcquireShared 返回值决定“是否唤醒后继”,不是“是否成功”
这是最容易误解的地方。很多人以为返回 0 就是“获取成功”,但实际它只表示“本次拿完了,后面别传了”。真实判断逻辑在 acquireShared 主流程里:
-
CountDownLatch.tryAcquireShared:若state == 0返回 0;否则返回负数。它从不返回正数——因为倒计时只有“完成”和“未完成”两种状态,没有“还剩一点能分”的中间态 -
Semaphore.tryAcquireShared(非公平):若state >= acquires,则 CAS 尝试减去acquires,成功后返回state - acquires(可能 > 0)。这个正数就是传播信号:告诉 AQS “我拿了,但后面还有剩,快唤醒下一个试试”
所以 CountDownLatch.await() 不会触发传播唤醒,而 Semaphore.acquire() 在许可充足时会连续唤醒多个等待线程——差异就藏在这个返回值里,不是实现不同,而是业务语义不同。
doReleaseShared 的自旋传播是共享模式吞吐的关键
无论是 countDown() 还是 release(),最终都走到 doReleaseShared()。它的核心动作是:
- 检查头节点后继是否为
SHARED模式节点 - 尝试
unpark它 - 如果此时
state仍满足后续获取条件(比如Semaphore还有剩余许可),就继续循环,尝试唤醒再下一个
这个循环不是“唤醒所有”,而是“唤醒一个,然后看状态要不要再唤醒一个”。它避免了惊群,也防止漏唤醒。你在线程 dump 里看到多个线程几乎同时从 park 返回,大概率就是这里在传播。
真正容易被忽略的是:共享模式下,state 的更新和唤醒不是原子绑定的。tryReleaseShared 只负责改 state 并返回 true/false;传播唤醒由 doReleaseShared 独立承担——这意味着你自定义共享同步器时,tryReleaseShared 里绝不能做 unpark,也不能假设 state 更新完就一定有人被唤醒。











