countdownlatch 的 await() 能阻塞多个线程,因其基于 aqs 共享模式,通过 cas 检查 state 是否为 0 决定是否入队挂起;countdown() 递减 state 至 0 时唤醒所有等待线程;state 归零后不可重置,故不可重用。

CountDownLatch 的 await() 为什么能阻塞多个线程?
因为 await() 底层调用的是 AQS 的 acquireSharedInterruptibly(1),而该方法在 tryAcquireShared() 返回负值时,会把当前线程封装为 Node.SHARED 节点加入 CLH 同步队列,并挂起——不是靠轮询,是靠 LockSupport.park() 真正阻塞线程。
关键点在于:tryAcquireShared() 只检查 getState() == 0:等于 0 就返回 1(表示获取成功),否则返回 -1(失败)。所以只要计数没归零,所有调用 await() 的线程都会入队等待。
- 多个线程同时调用
await(),可能在不同时间点进入doAcquireSharedInterruptibly(),但都遵循同一套入队 + 检查前驱是否为 head 的逻辑 - 没有“抢锁”竞争,只有“等开门”:它们共享同一个状态(state),不互斥,也不需要重入支持
- 一旦 state 变为 0,后续所有新来的
await()会直接返回,不再入队
countDown() 是如何触发唤醒的?
countDown() 实际调用的是 sync.releaseShared(1),它会执行 tryReleaseShared() —— 这个方法用 CAS 循环递减 state,直到成功;如果递减后 state == 0,就调用 doReleaseShared() 唤醒整个共享队列。
注意:doReleaseShared() 不是只唤醒一个线程,而是自旋遍历队列,对每个处于 SIGNAL 状态的后继节点调用 unpark(),确保所有等待线程都被释放。
- 唤醒过程不保证顺序,但 AQS 的
setHeadAndPropagate()机制会尝试向后传播唤醒信号,避免漏唤醒 - 即使多个线程几乎同时
countDown(),CAS 保证 state 递减原子性,不会出现“少减一次”导致永远阻塞 - 如果某个
countDown()正好把 state 减到 0,它承担唤醒责任;其他线程的countDown()则只做递减,不唤醒
为什么 CountDownLatch 不能重用?
因为它的 Sync 类中 tryAcquireShared() 和 tryReleaseShared() 都是单向设计:state 归零后,tryAcquireShared() 永远返回 1,tryReleaseShared() 对已为 0 的 state 不再做任何事(CAS 失败直接返回 false)。
换句话说,AQS 的 state 在这里纯粹是“一次性门闩”语义:开过一次,就再不会关上。没有重置逻辑,也没有重入或循环语义支持。
- 试图在 state=0 后再次
await(),会立刻返回,不阻塞 - 试图在 state=0 后继续
countDown(),无副作用,也不会报错 - 若需重复屏障,必须新建
CountDownLatch实例,或改用CyclicBarrier
任务对齐场景下容易踩的坑
所谓“任务对齐”,是指多个子任务并行执行、主线程等待全部完成后再推进——这正是 CountDownLatch 最典型的用途。但实际写的时候,几个细节极易出错:
- 忘记在
catch (InterruptedException)中恢复中断状态(应调用Thread.currentThread().interrupt()),导致上层无法感知中断 - 子线程中未把
countDown()放在finally块里,一旦任务抛异常,latch 少减一次,主线程永久阻塞 - 误以为
getCount()可用于“轮询等待”,其实它只是快照值,不能替代await();而且频繁调用还破坏了 AQS 的等待语义 - 在非主线程中调用
await()后未处理超时,导致整个线程池被卡死(尤其在线程复用场景下)
最隐蔽的问题是:state 是 int 类型,最大值 2³¹−1,但极少有人真去初始化一个上亿的 latch——可一旦误传负数或 0,new CountDownLatch(0) 会让所有 await() 立即返回,表面看“运行正常”,实则完全跳过了同步逻辑。











