在日常开发中,我们经常会遇到需要协调多个线程任务的场景。比如,主线程需要等待所有子线程处理完毕后再进行汇总,或者多个线程需要等待某个初始化操作完成后才能开始工作。直接控制线程的等待与唤醒既复杂又容易出错。
今天,我们来介绍一个非常实用的并发工具------CountDownLatch,它可以优雅地解决上述问题。
一、CountDownLatch 是什么?
CountDownLatch 是 java.util.concurrent 包下的一个同步工具类。它允许一个或多个线程等待其他一组线程完成操作。
你可以把它理解为一个计数器 ,这个计数器的初始值在创建时被设定。线程通过调用 await() 方法进入等待状态,直到计数器的值被其他线程通过 countDown() 方法减到零,等待的线程才会被唤醒继续执行。
它的核心特点非常明确:
- 计数单向性:计数器只能减少,不能增加。
- 一次性 :当计数器减到零后,再调用
await()方法会立即返回,无法重置再次使用。
二、核心方法与工作机制
CountDownLatch 的 API 非常简单,主要依赖两个核心方法:
await():使当前线程进入等待状态,直到计数器变为零。除非线程被中断。countDown():将计数器的值减 1。如果计数器达到零,将释放所有等待的线程。
工作机制简述:
- 初始化
CountDownLatch对象,设定一个正整数作为计数器初始值(例如N)。 - 主线程调用
await()方法后会被阻塞。 - 每个子线程在执行完任务后,调用
countDown()方法,使计数器减 1。 - 当所有
N个子线程都执行完毕并调用了countDown()后,计数器归零,主线程被唤醒并继续执行。
三、主要应用场景
CountDownLatch 在实际项目中有着广泛的应用,以下是一些典型场景:
1. 主线程等待所有子任务完成
这是最经典的场景。将一个大型任务拆分为多个子任务并行处理,主线程需要等待所有子任务完成后,才能进行结果合并或后续操作。
scss
// 模拟一个需要处理5个子任务的主线程
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
// 模拟子任务执行
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 完成任务");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 任务完成,计数器减1
latch.countDown();
}
}).start();
}
// 主线程等待所有子任务完成
latch.await();
System.out.println("所有子任务已完成,主线程开始汇总结果...");
2. 实现多个线程的并发启动
在某些性能测试或需要"同时发令"的场景下,可以使用 CountDownLatch 作为发令枪,让多个线程在同一时刻开始执行任务。
scss
// 发令枪,初始为1,所有线程都在等待这一声枪响
CountDownLatch startSignal = new CountDownLatch(1);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 所有线程都在此等待
startSignal.await();
// 收到信号,开始真正的工作
doWork();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// 主线程准备完毕后,鸣枪
Thread.sleep(1000); // 模拟准备时间
startSignal.countDown(); // 所有等待线程同时开始执行
3. 等待共享资源初始化
当多个线程依赖于一个共享资源,而这个资源初始化较慢时,可以让这些线程等待资源初始化完成后再开始工作。
scss
// 资源初始化计数器,初始为1
CountDownLatch resourceLatch = new CountDownLatch(1);
// 模拟资源初始化线程
new Thread(() -> {
initializeSharedResource(); // 耗时的初始化操作
resourceLatch.countDown(); // 初始化完成,释放所有等待线程
}).start();
// 其他工作线程
new Thread(() -> {
resourceLatch.await(); // 等待资源就绪
useSharedResource(); // 使用已初始化的资源
}).start();
四、重要注意事项
- 计数器无法重置 :
CountDownLatch是一次性的,计数器归零后就无法再次使用。如果需要循环使用的场景,请考虑使用CyclicBarrier。 - 确保计数器被减少 :务必在子线程中使用
try-finally块调用countDown(),以确保即使线程执行异常,计数器也能被减少,避免主线程无限期等待。 - 与 Spring 事务的陷阱 :在 Spring 管理的多线程场景中需要特别注意。
@Transactional注解的事务控制是基于ThreadLocal的,它不会跨线程传播。这意味着:
-
- 主线程的事务和子线程的事务是完全独立的。
- 如果主线程先执行了数据库操作,然后子线程在执行中抛出异常,主线程的操作不会回滚,这会导致数据不一致。
- 解决方案通常是将子线程的业务也包装在独立的事务中,并通过更复杂的机制(如事务事件监听、分布式事务)来协调,而不能简单地依赖
CountDownLatch。
总结
CountDownLatch 是一个设计精巧、功能明确的线程同步工具。它通过一个简单的计数器模型,高效地解决了"一个线程等待多个线程"和"多个线程等待一个信号"的协作问题。理解其原理并正确使用它,能够让你的多线程程序更加健壮和高效。
希望这篇文章能帮助你更好地理解和使用 CountDownLatch。