CountDownLatch解析:Java并发编程神器
欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。
大家好!今天我们来聊聊Java并发编程中一个非常实用的工具 —— CountDownLatch。
如果你在面试中被问到“如何让主线程等待多个子线程执行完成”,或者在实际开发中遇到需要协调多个线程的场景,那么CountDownLatch绝对是你的得力助手!
1. 什么是CountDownLatch?
想象一下这样的场景:一辆旅游大巴要发车,司机必须等所有乘客都上车后才能启动。这里的“乘客数量”就是CountDownLatch的“计数阈值”,乘客上车一次计数减1,减到0,司机就能发车。
CountDownLatch是Java并发包(java.util.concurrent)中的一个同步辅助类,它允许一个或多个线程等待其他线程完成操作后再继续执行。你可以把它理解为一个倒计时门闩,当计数归零时,门闩打开,等待的线程得以继续执行。
2. 核心原理:基于AQS的巧妙设计
CountDownLatch的底层实现依赖于Java并发框架的核心 —— AQS(AbstractQueuedSynchronizer)。几乎所有JUC包中的并发工具(如ReentrantLock、Semaphore等)都基于AQS实现。
2.1 源码核心剖析
CountDownLatch的代码非常简洁,所有核心逻辑都封装在内部的Sync类中:
public class CountDownLatch {
// 内部同步器,继承AQS
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count); // AQS的state就是CountDownLatch的计数
}
// 核心方法1:判断是否能继续执行
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 核心方法2:计数递减
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc)) {
return nextc == 0; // 只有减到0时才返回true
}
}
}
}
}
工作原理简述:
-
初始化:创建CountDownLatch时设置初始计数值,存入AQS的state变量
-
await():调用await()的线程会检查state是否为0,不为0则进入AQS等待队列被挂起
-
countDown():每个工作线程完成任务后调用countDown(),通过CAS原子操作将state减1
-
唤醒:当state减到0时,唤醒所有在等待队列中的线程
3. 核心API与使用步骤
CountDownLatch的API非常简单,主要包含构造函数和三个方法:
-
CountDownLatch(int count):构造函数,设置初始计数值
-
await():使当前线程等待,直到计数器变为0
-
countDown():将计数器减1
-
await(long timeout, TimeUnit unit):带超时的等待方法
标准使用步骤:
// 1. 创建CountDownLatch对象,指定需要等待的任务数
CountDownLatch latch = new CountDownLatch(3);
// 2. 启动多个工作线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 执行具体任务
doWork();
} finally {
// 3. 任务完成后调用countDown()
latch.countDown();
}
}).start();
}
// 4. 主线程等待所有任务完成
latch.await();
System.out.println("所有任务已完成!");
4. 实战应用场景
4.1 场景1:主线程等待多个子任务完成
这是最经典的用法。比如解析Excel的多个sheet,每个sheet用一个线程处理,主线程需要等待所有sheet解析完成后统计总耗时。
public class ExcelParser {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
long startTime = System.currentTimeMillis();
// 解析sheet1
new Thread(() -> {
parseSheet("sheet1", 2000);
latch.countDown();
}).start();
// 解析sheet2
new Thread(() -> {
parseSheet("sheet2", 5000);
latch.countDown();
}).start();
latch.await();
long endTime = System.currentTimeMillis();
System.out.println("总耗时:" + (endTime - startTime) + "ms");
}
}
4.2 场景2:模拟并发测试的“发令枪”
需要多个线程同时开始执行测试时,可以使用两个CountDownLatch:
// 发令枪:所有线程等待开始信号
CountDownLatch startSignal = new CountDownLatch(1);
// 完成信号:主线程等待所有线程完成
CountDownLatch doneSignal = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
startSignal.await(); // 等待开始信号
doTest();
} finally {
doneSignal.countDown();
}
}).start();
}
// 确保所有线程就绪后,同时开始
Thread.sleep(100);
startSignal.countDown(); // 鸣枪,所有线程同时开始
doneSignal.await(); // 等待所有线程完成
4.3 场景3:服务启动依赖管理
在微服务架构中,应用启动时需要等待多个服务初始化完成:
public class ServiceStarter {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
// 初始化数据库服务
new Thread(() -> {
initDatabase();
latch.countDown();
}).start();
// 初始化缓存服务
new Thread(() -> {
initCache();
latch.countDown();
}).start();
// 初始化消息队列
new Thread(() -> {
initMQ();
latch.countDown();
}).start();
latch.await();
System.out.println("所有服务初始化完成,应用启动成功!");
}
}
5. CountDownLatch vs CyclicBarrier:如何选择?
很多同学容易混淆CountDownLatch和CyclicBarrier,这里做个清晰对比:
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 核心思想 | 一个/多个线程等待其他线程完成 | 多个线程互相等待,到达屏障点后一起继续 |
| 重用性 | 一次性,计数归零后无法重置 | 可循环使用,自动重置 |
| 适用模型 | 主从模式(主线程等待工作线程) | 对等协作模式(所有线程地位平等) |
| 额外功能 | 无 | 支持barrierAction(屏障触发时执行的任务) |
| 异常处理 | 简单 | 复杂,某线程异常会导致屏障损坏 |
选择口诀:
-
等别人干完活 → 用CountDownLatch
-
大家齐步走 → 用CyclicBarrier
6. 最佳实践与避坑指南
6.1 务必在finally块中调用countDown()
try {
doWork();
} finally {
latch.countDown(); // 确保即使发生异常也能减少计数
}
6.2 使用带超时的await()避免永久阻塞
// 最多等待3秒
if (latch.await(3, TimeUnit.SECONDS)) {
// 所有任务完成
} else {
// 超时处理
System.err.println("任务执行超时!");
}
6.3 合理设置初始计数值
初始计数值应与实际需要等待的任务数严格一致,避免因多减或少减导致程序逻辑错误。
6.4 不要滥用CountDownLatch
对于简单的线程等待,也可以使用Thread.join(),但CountDownLatch更加灵活,不要求等待的线程必须结束。
6.5 与线程池配合使用
在实际项目中,通常配合线程池使用CountDownLatch:
ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
doTask();
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
7. 性能优化建议
-
避免频繁创建:CountDownLatch是一次性的,如果需要重复使用类似功能,考虑使用CyclicBarrier
-
合理设置线程数:根据任务类型和系统资源设置合适的线程数
-
监控等待时间:在生产环境中监控await()的等待时间,及时发现性能瓶颈
8. 总结
CountDownLatch是Java并发编程中不可或缺的工具,它通过简单的计数器模型,优雅地解决了“一个线程等待多个线程”的协作问题。其设计精巧、API简洁,是处理多线程同步的利器。
关键要点回顾:
-
CountDownLatch基于AQS实现,state变量存储计数值
-
await()使线程等待,countDown()减少计数
-
计数归零时唤醒所有等待线程
-
一次性使用,不可重置
-
务必在finally中调用countDown()避免死锁
-
与CyclicBarrier根据场景选择使用
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)