CountDownLatch 超详细完整版讲解(Java AQS 底层 + 源码 + 场景 + 踩坑 + 对比)

CountDownLatch 超详细完整版讲解(Java AQS 底层 + 源码 + 场景 + 踩坑 + 对比) 一、名词正确类名CountDownLatch包路径java.util.concurrent.CountDownLatch二、核心定义1. 本质同步闭锁Latch基于 AQSAbstractQueuedSynchronizer共享模式实现的线程等待工具。 闭锁一扇只能打开一次的门。初始化时设置一个计数器 state线程调用countDown()→ state state - 1任意线程调用await()会阻塞直到 state 0门打开所有等待线程全部放行一旦 state 归 0永久无法恢复不能重复使用2. 两大核心角色等待线程调用await()阻塞等待计数器归零通常是主线程工作线程执行业务完成后调用countDown()扣减计数三、完整 API 全解1. 构造方法public CountDownLatch(int count)count计数器初始值代表需要等待完成的任务 / 线程数量限制count 不能 0否则直接抛出IllegalArgumentException2. countDown()public void countDown()执行逻辑内部调用 AQS 共享释放releaseShared(1)将同步状态 state - 1如果减完后 state 0唤醒所有阻塞在await()的线程如果 state 已经是 0调用此方法无任何效果不会抛出中断异常3. await () 无超时阻塞public void await() throws InterruptedException阻塞当前线程直到 state0 才返回如果等待途中线程被interrupt()直接抛出InterruptedException清除中断标记使用场景确定所有任务一定会完成不考虑超时4. await (long timeout, TimeUnit unit) 带超时阻塞public boolean await(long timeout, TimeUnit unit) throws InterruptedException返回值规则true在超时时间内 state 变为 0全部任务完成false超时到了 state 仍大于 0有任务未完成 参数timeout时长数值unit时间单位TimeUnit.SECONDS/MILLISECONDS作用防止死锁永久阻塞生产环境必用5. getCount()public long getCount()返回当前剩余未扣减的计数值即当前 AQS 的 state 值。 注意返回值是瞬时快照多线程并发下读取的值不保证实时准确仅用于日志 / 监控。四、底层 AQS 源码拆解精简核心逻辑CountDownLatch 内部静态内部类Sync继承 AQSprivate static final class Sync extends AbstractQueuedSynchronizer { // 初始化同步状态state count Sync(int count) { setState(count); } // 获取当前剩余计数 int getCount() { return getState(); } // 共享式获取锁await底层调用 protected int tryAcquireShared(int acquires) { // state 0 返回1获取成功不阻塞 // state 0 返回-1获取失败进入阻塞队列 return (getState() 0) ? 1 : -1; } // 共享式释放锁countDown底层调用 protected boolean tryReleaseShared(int releases) { // CAS自旋循环扣减state for (;;) { int c getState(); // 已经是0无需处理 if (c 0) return false; int nextc c - 1; // CAS更新state if (compareAndSetState(c, nextc)) // 扣减后等于0返回true触发唤醒所有等待线程 return nextc 0; } } }底层流程总结await()→tryAcquireShared()state0线程进入 AQS 阻塞队列挂起countDown()→tryReleaseShared()CAS 自旋 state-1若 state0唤醒队列里所有等待线程全部恢复运行五、两种经典业务实战案例案例 1主线程等待多子线程全部执行完成批量查询关键点countDown()必须放在finally防止任务异常导致计数不扣减、主线程卡死import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class LatchBatchQueryDemo { public static void main(String[] args) throws InterruptedException { // 3个并行查询任务 int taskSize 3; CountDownLatch latch new CountDownLatch(taskSize); for (int i 1; i taskSize; i) { int taskId i; new Thread(() - { try { System.out.println(任务 taskId 开始查询数据); // 模拟数据库/远程接口耗时 TimeUnit.SECONDS.sleep(2); System.out.println(任务 taskId 查询完成); } catch (InterruptedException e) { System.out.println(任务 taskId 被中断); } finally { // 无论正常/异常都必须扣减计数 latch.countDown(); System.out.println(任务 taskId 扣减计数剩余 latch.getCount()); } }).start(); } System.out.println(主线程等待所有查询任务结束...); // 最多等待5秒避免永久阻塞 boolean allFinish latch.await(5, TimeUnit.SECONDS); if (allFinish) { System.out.println(全部查询完成合并数据返回); } else { System.out.println(部分任务超时未完成剩余计数 latch.getCount()); } } }案例 2压测场景 —— 所有线程同时并发执行双 Latch 经典用法需求10 个线程先全部就绪收到统一信号后同时发起请求模拟并发峰值startLatch放行门初始 1主线程 countDown 一次所有工作线程同时启动endLatch结束门初始 10主线程等待所有线程执行完毕import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class LatchPressureTestDemo { public static void main(String[] args) throws InterruptedException { int threadNum 10; // 启动闸门控制所有线程同时开始 CountDownLatch startLatch new CountDownLatch(1); // 结束闸门主线程等待全部线程跑完 CountDownLatch endLatch new CountDownLatch(threadNum); for (int i 0; i threadNum; i) { new Thread(() - { try { System.out.println(Thread.currentThread().getName() 已就绪等待统一执行信号); // 全部阻塞在这里等主线程开门 startLatch.await(); // 并发业务逻辑 System.out.println(Thread.currentThread().getName() 发起请求); TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } finally { endLatch.countDown(); } }).start(); } TimeUnit.SECONDS.sleep(1); System.out.println( 统一下发执行信号 ); // 闸门打开所有线程同时执行 startLatch.countDown(); // 等待所有压测线程结束 endLatch.await(); System.out.println(所有并发请求执行完毕); } }六、四大典型业务使用场景多数据源并行查询同时调用多个微服务、多库查询全部拿到结果后再统一组装返回缩短接口 RT。系统启动初始化Spring 服务启动时多线程加载缓存、初始化连接池、拉取配置主线程等待所有初始化完成再开启端口对外提供服务。接口并发压测双 Latch 实现精准同一时刻批量请求消除线程逐个启动带来的时间差。多任务分片处理大文件分片、批量数据分片分片线程全部处理完成后主线程做汇总、归档。七、核心特性重点区分同类工具计数器单向不可逆countDown 只能递减state 一旦到 0无法重置。如果需要循环复用必须使用CyclicBarrier。等待线程无数量限制可以 N 个线程同时调用await()阻塞计数器归零后全部一起唤醒。工作线程与等待线程完全解耦CountDownLatch 不要求等待线程和工作线程一一对应主线程单独等待一批子线程是标准用法。不支持任务回调计数器归 0 后没有内置执行回调方法CyclicBarrier 支持构造传入屏障回调任务。八、CountDownLatch vs CyclicBarrier vs Semaphore 完整对比表对比维度CountDownLatchCyclicBarrierSemaphore核心作用一组线程完成后唤醒等待线程一组线程互相等待全部到达屏障再放行控制同一时间并发线程数量限流计数器单向递减归零作废不可复用循环复用每次凑齐指定数量自动重置许可 acquire 减少、release 增加可循环等待主体外部主线程等待工作线程所有线程互相等待对方线程抢许可无等待分组逻辑回调支持无内置回调支持屏障完成执行回调 Runnable无回调底层 AQS 模式共享锁独占锁 重入机制共享锁典型场景批量任务汇总、启动等待、压测闸门多阶段分段并行计算接口限流、连接池控制九、生产环境高频踩坑点 解决方案坑 1countDown () 未放入 finally任务异常导致永久阻塞错误写法// 错误发生异常直接跳过countDown try { doBiz(); latch.countDown(); } catch (Exception e) { log.error(异常); }修复countDown () 固定写在 finally 块无论是否异常都扣减计数。坑 2初始化 count 和实际 countDown 调用次数不匹配count5但只调用 3 次 countDown → state 永远 0await 死锁count3但调用 5 次 countDown前 3 次归零后 2 次无效逻辑正常但浪费调用 解决严格保证每个工作线程对应一次 countDown线程数量 count 初始值。坑 3生产环境直接使用无超时 await ()服务器任务阻塞、线程卡死时主线程永久挂起耗尽线程池资源。 强制规范业务代码一律使用await(time, unit)超时版本增加兜底逻辑。坑 4多轮循环复用同一个 CountDownLatch循环第二次时 state 已经是 0await 直接放行失去等待效果。 重复等待场景改用 CyclicBarrier。坑 5忽略 InterruptedException 不处理线程中断后直接抛出异常流程中断建议捕获异常后主动执行 countDown 释放计数。坑 6用 getCount () 做业务判断getCount 只是瞬时快照并发下数据不准确仅用于打印日志监控不要作为业务分支判断条件。十、扩展补充CountDownLatch 与线程池搭配最佳实践实际项目不会手动 new Thread统一搭配ThreadPoolExecutorExecutorService pool Executors.newFixedThreadPool(5); CountDownLatch latch new CountDownLatch(5); for (int i 0; i 5; i) { pool.submit(() - { try { // 业务逻辑 } finally { latch.countDown(); } }); } latch.await(10, TimeUnit.SECONDS); pool.shutdown();十一、总结一句话记忆CountDownLatch 一次性单向门闩主线程等待 N 个工作线程全部扣完计数后统一放行适合一次性批量等待场景不可循环复用生产必须加超时、countDown 写在 finally 防止死锁。