- 引言
- 一、为什么需要CountDownLatch?
- 二、核心概念与工作原理
- 三、使用方式与API详解
- 四、经典应用场景
- 五、底层原理简析
- 六、总结与最佳实践
- 互动环节
引言
在多线程编程中,我们经常遇到这样的需求:需要等待多个线程都完成任务后,主线程才能继续执行。比如,主线程启动多个工作线程去加载系统所需的各项资源(数据库连接、缓存数据、配置文件等),必须等所有资源都加载完毕,主线程才能宣布系统启动完成。
如果用简单的Thread.join()来实现,代码会变得臃肿且难以维护。而
java.util.concurrent.CountDownLatch(倒计时门闩)正是JDK为我们提供的优雅解决方案。它就像赛跑时的发令枪,所有运动员(子线程)准备就绪,等待枪响(计数器归零)后同时出发;或者像终点线,等待所有运动员冲线后比赛才结束。
一、为什么需要CountDownLatch?
想象一下以下几个场景:
- 游戏大厅:一个主线程需要等待所有玩家(子线程)都加载完地图和资源后,游戏才能开始。
- 压力测试:需要同时启动成千上万个线程去访问一个服务,以模拟高并发场景。
- 数据汇总:启动多个线程计算数据的不同部分,主线程需要等待所有部分计算完成后,才能进行汇总。
在这些场景下,核心需求是:一个或多个线程需要等待其他一组线程完成操作。
传统方法的弊端:
- 使用Thread.join():需要持有每个线程的引用,并且无法复用。
- 使用while循环忙等待:浪费CPU资源,效率低下。
CountDownLatch的优势在于它提供了一种标准化的、高效的、可复用的线程等待机制。
二、核心概念与工作原理
核心思想:CountDownLatch通过一个计数器(count) 来实现同步。这个计数器在初始化时被设定为一个正数(通常是需要等待的线程数量)。
两大核心操作:
- 倒计时(Count Down):每个完成任务的工作线程调用countDown()方法,计数器会减1。
- 等待(Await):等待的线程(通常是主线程)调用await()方法。该方法会阻塞,直到计数器减到0,所有等待的线程会被释放,继续执行。
重要特性:
- 一次性:计数器的值不能被重置。一旦计数器为0,所有对await()的调用都会立即通过,无法再次使用。如果需要重置,请考虑使用CyclicBarrier。
- 不可逆:计数器只能减少,不能增加。
三、使用方式与API详解
1. 核心API
- CountDownLatch(int count):构造函数,参数count为需要倒数的次数。
- void await():调用此方法的线程会被挂起,它会等待直到count值为0才继续执行。等待线程调用。
- boolean await(long timeout, TimeUnit unit):与await()类似,但增加了超时时间。如果在指定时间内count值还没变为0,也会不再等待。
- void countDown():将count值减1。如果减1后count值为0,则唤醒所有等待的线程。工作线程调用。
- long getCount():获取当前的计数器值。
2. 基础用法示例
import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class BasicCountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // 模拟有3个任务需要完成 int workerCount = 3; // 1. 创建CountDownLatch,初始化计数器为3 CountDownLatch latch = new CountDownLatch(workerCount); for (int i = 1; i <= workerCount; i++) { final int taskId = i; new Thread(() -> { try { // 模拟任务执行耗时 System.out.println("任务" + taskId + "正在执行..."); Thread.sleep((long) (Math.random() * 2000)); System.out.println("任务" + taskId + "执行完毕!"); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 2. 每个任务完成后,调用countDown(),计数器减1 // 务必放在finally块中,确保无论任务成功与否都会执行 latch.countDown(); } }).start(); } System.out.println("主线程等待所有任务完成..."); // 3. 主线程调用await(),阻塞等待,直到计数器变为0 latch.await(); // 4. 所有任务完成后,主线程继续执行 System.out.println("所有任务均已完成,主线程开始进行汇总工作..."); } }
输出可能为:
主线程等待所有任务完成... 任务1正在执行... 任务2正在执行... 任务3正在执行... 任务1执行完毕! 任务3执行完毕! 任务2执行完毕! 所有任务均已完成,主线程开始进行汇总工作...
3. 带超时的等待
在实际生产中,为了避免线程无限期等待(比如某个子线程死锁或执行时间过长),建议使用带超时的await。
// 在主线程中 try { // 最多等待5秒 boolean allDone = latch.await(5, TimeUnit.SECONDS); if (allDone) { System.out.println("所有任务在5秒内完成!"); } else { System.out.println("警告:有任务超时未完成!当前已完成任务数: " + (workerCount - latch.getCount())); // 这里可以执行一些超时处理逻辑,比如取消剩余任务、记录日志等 } } catch (InterruptedException e) { e.printStackTrace(); }
四、经典应用场景
场景一:模拟高并发(同时开始)
让所有线程在同一时刻开始执行,模拟真正的并发压力测试。
public class ConcurrentStartDemo { public static void main(String[] args) throws InterruptedException { int threadCount = 100; CountDownLatch startSignal = new CountDownLatch(1); // 开始信号,初始为1 CountDownLatch doneSignal = new CountDownLatch(threadCount); // 完成信号 for (int i = 0; i < threadCount; i++) { new Thread(() -> { try { // 所有线程启动后,都在此等待“开始信号” startSignal.await(); // 等待主线程“发令” // 模拟真正要测试的业务逻辑 doBusinessLogic(); } catch (InterruptedException e) { e.printStackTrace(); } finally { doneSignal.countDown(); } }).start(); } System.out.println("所有线程准备就绪..."); Thread.sleep(2000); // 模拟主线程做一些准备工作 System.out.println("发令枪响!所有线程同时开始执行!"); startSignal.countDown(); // 关键一步:将开始信号计数器减为0,释放所有等待的线程 doneSignal.await(); // 等待所有线程执行完毕 System.out.println("所有线程执行完成!"); } private static void doBusinessLogic() { // ... 业务代码 ... } }
场景二:等待所有资源初始化(全部结束)
这是最常用的场景,主线程等待所有辅助线程完成初始化工作。
public class ResourceInitializationDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); // 3个初始化任务 // 初始化缓存 new Thread(() -> { initCache(); latch.countDown(); }).start(); // 初始化数据库连接池 new Thread(() -> { initDatabasePool(); latch.countDown(); }).start(); // 加载配置文件 new Thread(() -> { loadConfig(); latch.countDown(); }).start(); System.out.println("主线程等待所有初始化任务完成..."); latch.await(); // 阻塞,直到3个任务的countDown()都被调用 System.out.println("所有资源初始化完毕,系统启动成功!"); // ... 启动服务器等后续操作 } private static void initCache() { /* ... */ } private static void initDatabasePool() { /* ... */ } private static void loadConfig() { /* ... */ } }
五、底层原理简析
CountDownLatch的实现是基于AQS(
AbstractQueuedSynchronizer) 这个强大的同步器框架。
- 同步状态(state):在AQS中,有一个volatile的int state变量。在CountDownLatch中,这个state就代表了计数器的值。
- await()方法:当线程调用await()时,它会去检查state是否为0。如果不是0,当前线程就会被构造成一个Node节点,通过AQS的机制加入到等待队列中,并被挂起(通过LockSupport.park())。
- countDown()方法:调用countDown()会使用CAS操作将state减1。如果CAS成功后发现state变成了0,就会去唤醒等待队列中的所有线程(通过LockSupport.unpark())。
正是基于AQS,CountDownLatch才能高效、线程安全地管理计数器和线程的阻塞与唤醒。
六、总结与最佳实践
- 核心作用:CountDownLatch是一个同步辅助工具,用于协调多个线程之间的执行节奏,允许一个或多个线程等待另一组线程完成操作。
- 主要方法:await()用于等待,countDown()用于发出“完成一个”的信号。
- 一次性:计数器无法重置,用完即废。如果需要循环使用,应考虑CyclicBarrier。
- 最佳实践:
- 务必在finally中countDown:确保即使任务执行过程中抛出异常,计数器也能被减少,避免主线程永远等待。
- 使用带超时的await:防止因为个别线程问题导致整个程序卡死,增强系统鲁棒性。
- 明确角色:分清谁是“等待者”(调用await),谁是“工作者”(调用countDown)。
- ** vs 其他工具**:
- Thread.join():CountDownLatch更灵活,不需要持有线程对象引用,且可以用于等待事件(而不一定是线程结束)。
- CyclicBarrier:CyclicBarrier是等所有线程都到达一个屏障点后才能继续,并且可以重置复用;而CountDownLatch是等一个事件发生N次(计数器减到0),且不能重置。
CountDownLatch是JUC包中最简单却最实用的同步工具之一。理解并熟练运用它,能让你在设计多线程程序时更加得心应手,轻松解决复杂的线程同步问题。