CountDownLatch 超详细完整版讲解(Java AQS 底层 + 源码 + 场景 + 踩坑 + 对比)
CountDownLatch 超详细完整版讲解(Java AQS 底层 + 源码 + 场景 + 踩坑 + 对比)
一、名词
正确类名:CountDownLatch 包路径:java.util.concurrent.CountDownLatch
二、核心定义
1. 本质
同步闭锁(Latch),基于 AQS(AbstractQueuedSynchronizer)共享模式实现的线程等待工具。 闭锁:一扇只能打开一次的门。
- 初始化时设置一个计数器 state
- 线程调用
countDown()→ state = state - 1 - 任意线程调用
await()会阻塞,直到 state == 0,门打开,所有等待线程全部放行 - 一旦 state 归 0,永久无法恢复,不能重复使用
2. 两大核心角色
- 等待线程:调用
await(),阻塞等待计数器归零(通常是主线程) - 工作线程:执行业务,完成后调用
countDown()扣减计数
三、完整 API 全解
1. 构造方法
public CountDownLatch(int count)
- count:计数器初始值,代表需要等待完成的任务 / 线程数量
- 限制:count 不能 < 0,否则直接抛出
IllegalArgumentException
2. countDown()
public void countDown()
执行逻辑:
- 内部调用 AQS 共享释放
releaseShared(1) - 将同步状态 state - 1
- 如果减完后 state == 0:唤醒所有阻塞在
await()的线程 - 如果 state 已经是 0,调用此方法无任何效果
- 不会抛出中断异常
3. await () 无超时阻塞
public void await() throws InterruptedException
- 阻塞当前线程,直到 state=0 才返回
- 如果等待途中线程被
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 继承 AQS:
private 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()- state>0:线程进入 AQS 阻塞队列,挂起
countDown()→tryReleaseShared()- CAS 自旋 state-1
- 若 state==0:唤醒队列里所有等待线程,全部恢复运行
五、两种经典业务实战案例
案例 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 完整对比表
| 对比维度 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 核心作用 | 一组线程完成后唤醒等待线程 | 一组线程互相等待全部到达屏障再放行 | 控制同一时间并发线程数量(限流) |
| 计数器 | 单向递减,归零作废,不可复用 | 循环复用,每次凑齐指定数量自动重置 | 许可 acquire 减少、release 增加,可循环 |
| 等待主体 | 外部主线程等待工作线程 | 所有线程互相等待对方 | 线程抢许可,无等待分组逻辑 |
| 回调支持 | 无内置回调 | 支持屏障完成执行回调 Runnable | 无回调 |
| 底层 AQS 模式 | 共享锁 | 独占锁 + 重入机制 | 共享锁 |
| 典型场景 | 批量任务汇总、启动等待、压测闸门 | 多阶段分段并行计算 | 接口限流、连接池控制 |
九、生产环境高频踩坑点 + 解决方案
坑 1:countDown () 未放入 finally,任务异常导致永久阻塞
错误写法:
// 错误:发生异常直接跳过countDown
try {
doBiz();
latch.countDown();
} catch (Exception e) {
log.error("异常");
}
修复:countDown () 固定写在 finally 块,无论是否异常都扣减计数。
坑 2:初始化 count 和实际 countDown 调用次数不匹配
- count=5,但只调用 3 次 countDown → state 永远 > 0,await 死锁
- count=3,但调用 5 次 countDown:前 3 次归零,后 2 次无效,逻辑正常但浪费调用 解决:严格保证每个工作线程对应一次 countDown,线程数量 = count 初始值。
坑 3:生产环境直接使用无超时 await ()
服务器任务阻塞、线程卡死时,主线程永久挂起,耗尽线程池资源。 强制规范:业务代码一律使用 await(time, unit) 超时版本,增加兜底逻辑。
坑 4:多轮循环复用同一个 CountDownLatch
循环第二次时 state 已经是 0,await 直接放行,失去等待效果。 重复等待场景改用 CyclicBarrier。
坑 5:忽略 InterruptedException 不处理
线程中断后直接抛出异常,流程中断,建议捕获异常后主动执行 countDown 释放计数。
坑 6:用 getCount () 做业务判断
getCount 只是瞬时快照,并发下数据不准确,仅用于打印日志监控,不要作为业务分支判断条件。
十、扩展补充:CountDownLatch 与线程池搭配最佳实践
实际项目不会手动 new Thread,统一搭配 ThreadPoolExecutor:
ExecutorService 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 防止死锁。

浙公网安备 33010602011771号