CountDownLatch不能重复使用,因其计数器归零后不可重置,后续await()直接返回,导致多轮测试失效;正确做法是每次循环新建实例。

CountDownLatch 为什么不能重复使用
因为 CountDownLatch 是一次性同步器,内部计数器归零后无法重置。一旦 countDown() 把计数减到 0,所有阻塞在 await() 的线程立刻放行,后续再调 await() 就直接返回,不再等待——这会让“多次循环断言”场景下后续轮次完全失效。
- 常见错误现象:
await()在第二轮测试里秒过,断言实际没等线程执行完就跑了,结果误判为“通过” - 使用场景:需要反复启动一批线程、每次等它们全部结束再校验状态(比如压力测试、并发正确性验证)
- 正确做法:每次循环都新建一个
CountDownLatch实例,计数值设为线程数 - 别用
reset()——它根本不存在;也别试图反射改sync内部状态,那是破坏契约
多线程断言失败时怎么定位是竞态还是逻辑错
并发测试里断言失败,不等于代码有 bug,很可能是你没等所有线程真正完成就去读共享状态了。
- 典型表现:
AssertionError偶发出现,且只在高并发或 CI 环境复现,本地单步调试却总通过 - 关键检查点:确认所有线程是否真的“退出”而非“暂停在临界区外”,比如用了
synchronized但锁还没释放完,或者用了AtomicInteger却忘了get()时机 - 实操建议:在
await()返回后,加一行Thread.sleep(10)(仅测试用),看断言是否稳定;如果加了就稳了,说明你漏等了某些异步副作用(比如日志刷盘、缓存写回) - 更可靠的做法:用
CompletableFuture.allOf(...).join()替代CountDownLatch,它天然绑定任务生命周期,不容易漏等
用 CountDownLatch 做并发执行控制的参数陷阱
构造 CountDownLatch 时传的数字,必须严格等于你显式调用 countDown() 的次数,且只能由工作线程调用——主线程不能代劳,也不能少调或多调。
- 容易踩的坑:
new CountDownLatch(5),但只起了 4 个线程,第 5 次countDown()被遗忘在某个异常分支里 → 主线程永远卡在await() - 安全写法:把
countDown()放进finally块,哪怕线程抛异常也要减一 - 别在循环里重复
latch.countDown():每个线程只应调一次,否则计数会提前归零 - 性能影响:
CountDownLatch本身无锁,开销极低;但若等待时间过长(比如几秒),说明线程卡死或资源争用严重,该查死锁或 I/O 阻塞了
多次循环测试并发代码时,共享状态怎么清干净
如果你在循环里复用同一个对象(比如静态 Map、全局 AtomicInteger),前一轮线程写入的数据会污染下一轮断言,导致“假失败”或“假通过”。
立即学习“Java免费学习笔记(深入)”;
- 典型错误:用
static Map<String, Integer> counter = new ConcurrentHashMap<>(),每轮测试都往里 put,但从不 clear - 正确做法:每次循环开始前,显式重置所有被并发线程修改的共享变量,例如
counter.clear()或重新 new 一个实例 - 更稳妥的方式:把被测逻辑封装成无状态方法,输入输出都走参数和返回值,避免依赖外部可变状态
- 注意 JVM 级别残留:比如用了
ThreadLocal却没remove(),线程复用(如从线程池来)会导致上一轮数据泄漏
CountDownLatch 的 await() 根本拦不住它们。








