CountDownLatch 的 countDown() 能被所有等待线程及时感知,根本原因在于 AQS 的 volatile state 字段与 CAS 操作共同保障了原子性和内存可见性,唤醒时通过 LockSupport.unpark() 确保状态变更立即可见。

CountDownLatch 的计数器递减(countDown())之所以能被所有等待线程及时感知,根本原因在于它天然保障了内存可见性——这不是靠开发者手动加 volatile 或 synchronized 实现的,而是由底层机制自动完成的。
计数器递减基于 AQS 与 volatile 语义
CountDownLatch 内部使用 AbstractQueuedSynchronizer(AQS)作为同步基础。其计数器实际存储在 AQS 的 state 字段中,该字段被声明为 volatile。每次调用 countDown(),本质是执行 CAS(Compare-And-Swap)操作:compareAndSetState(expect, update)。这个操作不仅保证原子性,还强制刷新写缓存、使变更对其他 CPU 核心立即可见。
- CAS 操作本身具有内存屏障语义,禁止相关读写指令重排序
-
volatile的写操作会触发 StoreStore 和 StoreLoad 屏障,确保之前所有写操作都已提交到主内存 - 等待线程在
await()中循环检查state == 0时,每次读取也都是 volatile 读,能拿到最新值
无需额外同步,避免手动可见性陷阱
如果用普通 int 变量模拟计数器,仅靠 synchronized 或 volatile 单独使用都不可靠:
- 只用
volatile int count:递减操作count--非原子,可能丢失更新 - 只用
synchronized:虽能保证原子性与可见性,但锁竞争开销大,且易因异常、遗漏 unlock 导致死锁或信号丢失 - CountDownLatch 把原子性(CAS)和可见性(volatile + 内存屏障)封装在一起,调用
countDown()即安全有效
可见性体现在 await 的响应时机上
当最后一个 countDown() 将计数器从 1 变为 0 时,所有正在 await() 的线程不会“轮询等待”或“延迟感知”。它们会在下一次检查 state 值时立刻看到 0,并退出阻塞状态。这种即时性不是运气,而是:
立即学习“Java免费学习笔记(深入)”;
- AQS 在计数归零后主动唤醒等待队列中的全部节点
- 唤醒过程通过 LockSupport.unpark() 实现,该操作本身具有内存可见性保证
- 被唤醒线程重新进入运行态后,首次读取 state 必然是 0,不会因缓存旧值而继续等待
对比 wait/notify 的可见性缺陷
若用 wait()/notify() 手动实现类似逻辑,必须依赖 synchronized 块才能保证可见性。但一旦出现以下情况,等待线程就可能永远看不到状态变化:
- 通知前未正确获取同一把锁,抛出 IllegalMonitorStateException
- 通知发生在等待线程进入
wait()之前(信号丢失) - 异常导致
notify()未执行,而wait()已开始阻塞
CountDownLatch 绕开了这些风险——它的“递减即信号”设计,让状态变更与通知动作合二为一,内存可见性由 JVM 底层机制兜底,不依赖程序员对锁和内存模型的精确控制。


















