Java-CountDownLatch原理及解析
CountDownLatch原理及解析
CountDownLatch基础概念
CountDownLatch可以理解为一个一次性的同步计数器。它内部维护了一个计数值:
- 初始化时指定计数大小
- 一个或多个线程调用
await()进入等待 - 其他线程通过
countDown()不断将计数减1 - 当计数减到
0时,所有等待线程继续向下执行
它解决的核心问题是:让某个线程(或某批线程)等待另外一些操作全部完成之后再继续执行。
例如:
- 主线程等待多个子任务执行完再汇总结果
- 某个初始化流程需要等多个前置模块准备完成后再开始
- 测试场景中需要等待若干异步任务都结束后再断言
需要注意的是,CountDownLatch更适合一次性协调场景。计数归零之后,它不会自动重置,如果想重复使用同一类同步屏障,更适合考虑CyclicBarrier这类组件。
它之所以是“一次性”的,本质原因就在于内部的state只会单向递减到0。AQS并不会在计数归零后自动帮它把状态恢复到初始值,所以同一个CountDownLatch对象完成一次协调任务后,就不能再回到初始状态重新使用。
CountDownLatch使用方式
CountDownLatch最常见的使用模式其实很固定:
- 初始化一个计数器
- 一个或多个线程调用
await()等待 - 其他线程在任务完成后调用
countDown() - 当计数归零时,等待线程继续执行
一个典型示例如下:
1 | |
上面的流程里,主线程会阻塞在latch.await()处,直到3个工作线程都执行完并分别调用一次countDown(),计数器减为0,主线程才会继续向下执行。
如果从使用语义上和Thread.join()做对比,也能更清楚地看到它的定位差异:
join()等待的是某个具体线程结束CountDownLatch等待的是某个计数条件归零
因此CountDownLatch并不要求“完成事件”一定来自线程本身,它等的可以是任务、阶段、回调,或者任何你愿意用countDown()表示的一次完成信号。
核心方法
await()
await()表示当前线程进入等待,直到计数归零。
如果调用时计数已经是0,那么线程不会阻塞,会直接继续执行;如果计数还大于0,当前线程就会进入等待状态。
await()本身是可中断的:如果线程在等待过程中被中断,就会抛出InterruptedException并结束等待,而不是继续无限阻塞。
它还有带超时参数的方法:
1 | |
表示在指定时间内等待计数归零,超时则返回false。
因此从等待结果上看,await()大致有三种结束方式:
- 计数正常归零,方法返回
- 等待过程中线程被中断,抛出
InterruptedException - 带超时版本等待超时,返回
false
countDown()
countDown()会把当前计数减1。
这里有两个很关键的点:
countDown()不会阻塞调用线程- 计数减到
0之后,再继续调用countDown()也不会变成负数
而且并不是每一次countDown()都会唤醒等待线程。只有最后一次把计数减到0时,AQS才会真正进入共享模式的传播唤醒流程;在那之前,等待线程依旧会继续阻塞。
也就是说,它更像是“上报一次完成事件”,而不是一个需要等待结果的同步方法。
getCount()
getCount()用于查看当前剩余计数。
这个方法更适合做调试、日志打印或状态观察,而不是作为业务线程安全控制的核心判断依据。因为在并发场景下,读到的计数值可能很快就会被其他线程改掉。
CountDownLatch底层原理
CountDownLatch底层是基于AQS(AbstractQueuedSynchronizer)的共享模式实现的。
如果从AQS视角来看,它的核心映射关系非常直接:
state:表示当前剩余计数await():对应共享获取资源countDown():对应共享释放资源
也就是说,CountDownLatch并不是自己从零维护一套等待唤醒逻辑,而是复用了AQS在共享模式下的排队、阻塞和传播唤醒能力。
state 的含义
CountDownLatch初始化时会把计数值设置到AQS的state里。
例如:
state = 3:还需要3次countDown()state = 1:还差最后一次state = 0:条件已经满足,等待线程可以继续执行
所以对CountDownLatch来说,state并不是锁重入次数,也不是许可证个数,而是剩余计数。
await() 对应共享获取
调用await()时,本质上会尝试以共享模式获取资源。
对于CountDownLatch来说,这里的“资源是否可获取”判断非常简单:
- 如果
state == 0,说明计数已经归零,获取成功,线程直接继续执行 - 如果
state > 0,说明条件还未满足,获取失败,线程进入AQS同步队列等待
因此,等待线程并不是在“等某个锁释放”,而是在等计数归零这个条件成立。
之所以这里要走共享模式而不是独占模式,关键就在于:一旦state == 0,并不是只允许某一个等待线程继续执行,而是所有等待线程都可以通过。所以这里的“共享”并不是多个线程同时访问某个共享资源,而是多个等待线程在条件满足后都可以被一起放行。
countDown() 对应共享释放
调用countDown()时,本质上是在共享模式下释放资源,也就是把state减1。
如果减完之后:
state > 0:说明计数还没归零,等待线程继续等待state == 0:说明条件已经满足,AQS开始唤醒同步队列中的等待线程
这也是为什么说CountDownLatch虽然不是传统意义上的共享资源访问器,但它在AQS里依然使用的是共享模式语义。
从实现主线去理解,countDown()可以粗略看成这样一条过程:
- 读取当前
state - 基于CAS尝试把
state减1 - 如果CAS失败,说明有并发更新,重新读取再重试
- 只有当某次更新真正把
state从1改到0时,才触发后续共享释放传播
所以countDown()真正关键的不是“调用了多少次这个方法”,而是哪一次把计数推进到了0。
从内存语义上看,还有一个很重要的结论:工作线程在调用countDown()之前对共享数据做的写入,对于那些因为计数归零而从await()成功返回的线程来说,是可见的。也正因为有这层happens-before关系,主线程在等待结束后再去读取多个子任务写入的结果,才有明确的可见性保障。
执行流程
把await -> countDown -> 计数归零 -> 唤醒等待线程这条主线串起来,大致可以分成下面几步:
1. 初始化计数
创建CountDownLatch时传入初始值,这个值会保存到AQS的state中。
1 | |
此时表示还需要3次countDown(),等待线程才能继续执行。
2. 调用await检查计数
线程调用await()时,会先检查当前state:
- 如果已经是
0,直接通过 - 如果大于
0,则进入AQS同步队列等待
这里等待线程会被挂起,不再继续执行后续代码。
3. 其他线程不断countDown
其他线程每完成一个子任务,就调用一次countDown()。
每调用一次,state都会减1:
3 -> 22 -> 11 -> 0
前两次减完之后,等待线程仍然不会被真正放行,因为条件还没有满足。
4. 最后一次countDown使计数归零
当最后一次countDown()把state减为0时,AQS会进入共享模式下的唤醒流程。
这时等待在同步队列中的线程不再需要继续阻塞,而是会被逐步唤醒,重新参与调度。
5. 等待线程继续执行
被唤醒的线程恢复执行后,await()返回,后续逻辑继续向下运行。
所以从执行语义上看,await()返回并不是表示“刚刚被某个线程通知”,而是表示:计数已经归零,并且当前线程已经从等待状态恢复。
这里还要补一个容易忽略的边界:如果等待线程在await()期间被中断,它只是自己提前结束等待,并不会影响门闩本身的计数状态。也就是说,中断不会帮你把countDown()“补回来”,计数器也不会自动回滚。
CountDownLatch与其他并发工具的区别
和 CyclicBarrier 的区别
CountDownLatch:更适合“等别人做完”CyclicBarrier:更适合“一组线程互相等待,全部到齐后一起继续”
并且CountDownLatch是一次性的,而CyclicBarrier在满足条件后通常可以继续复用。
和 Semaphore 的区别
CountDownLatch控制的是“剩余计数是否归零”Semaphore控制的是“同时允许多少线程访问资源”
两者都基于AQS共享模式,但表达的同步语义完全不同:一个偏阶段完成通知,一个偏并发流量控制。
如果再从业务语义上补一句,也可以这样记:
CountDownLatch只关心“结束了几个”Semaphore更关心“现在允许进去几个”
常见误用与实践建议
- 初始化计数必须和真实完成事件数量匹配,否则可能永远等不到归零。
- 子任务里调用
countDown()最好放在finally中,避免中途异常导致主线程永久等待。 - 如果只是等待某一个线程结束,优先考虑
join(),不必为了计数而额外引入门闩。 - 如果需要循环多轮使用同一个同步点,不要用
CountDownLatch硬撑,应该换成更适合复用的组件。
另外还要注意一点:CountDownLatch只表达“阶段结束”,并不自动区分“成功结束”还是“失败结束”。很多时候任务即使失败了,也依然应该在finally里执行countDown(),否则等待方可能永远卡住;至于任务到底成功还是失败,则需要额外的共享结果、异常收集或状态变量来表达。
一个很经典的实践方式是“双门闩”模式:
- 一个
startLatch控制所有工作线程统一开始 - 一个
doneLatch控制主线程等待所有工作线程全部结束
这样既可以让多线程尽量同时起跑,也可以在最后统一收口等待结果。
这种模式在并发压测、并发单测、批量任务统一起跑的场景里非常常见:
startLatch初始值通常为1,所有工作线程先await()等起跑信号- 主线程准备完成后执行一次
countDown(),所有工作线程同时放开 - 每个工作线程结束时对
doneLatch执行countDown() - 主线程最后再
await()等待全部任务结束
这样既能最大化模拟“同一时刻并发开始”,也能在结尾统一回收结果。
小结
可以把CountDownLatch理解成:基于AQS共享模式实现的一次性同步计数器。
理解这篇时,重点抓住下面这条主线就够了:
state表示剩余计数await()检查计数是否已经归零countDown()负责递减计数- 当
state == 0时,AQS 共享模式唤醒等待线程
它的最大特点有两个:
- 使用简单,非常适合“等待一批任务完成”场景
- 一次性使用,计数归零后不会自动重置
如果从使用习惯上再补一句,可以记成:
await()负责等countDown()负责报完成state == 0负责放行所有等待线程
如果再加一句最容易忽略的提醒,就是:
await()被中断不会回滚计数countDown()也不表示任务一定成功