Java并发编程JUC CountDownLatch线程同步详解
在Java多线程开发中,经常需要主线程等待一组子任务全部完成后才能继续执行后续逻辑。本文介绍如何使用java.util.concurrent包下的CountDownLatch实现这种线程同步控制,通过计数器和await/countDown方法协同工作,解决复杂的启动顺序问题。
CountDownLatch的核心概念
CountDownLatch是一个同步辅助类,它允许一个或多个线程等待其他线程完成一组操作。它的核心思想基于一个计数器:在创建时指定计数值(通常等于需要等待的线程数),每当一个参与等待的线程完成任务时,调用countDown()使计数器减一;当计数器归零时,所有处于await()状态等待的线程将被唤醒并继续执行。
作为Java 5引入JUC(java.util.concurrent)包的重要组成部分,CountDownLatch常与CyclicBarrier、Semaphore等同步工具配合使用,但其特性更为单一且专注:不可重复使用的倒计数门闸。这意味着一旦计数器归零,该实例即失效,若需复用需重新实例化,这一点与可循环使用的CyclicBarrier不同。
工作原理与操作流程
第1步:初始化计数器
在主线程中创建一个CountDownLatch对象,传入参数为正整数,表示需要等待的子线程数量。例如:
CountDownLatch latch = new CountDownLatch(n);
这里设置的数值即为倒计数的起点,且只能设置一次,后续无法重置或修改初始值。
第2步:启动工作线程并传递引用
将创建的CountDownLatch实例传递给各个子线程。子线程在执行完自己的业务逻辑后,必须调用countDown()方法。每调用一次,全局计数器减1。
伪代码逻辑如下:
- 主线程初始化CountDownLatch(count)。
- 启动N个子线程执行任务。
- 主线程调用latch.await()进入阻塞状态,直到count变为0。
- 每个子线程完成任务后调用latch.countDown()。
- 当第N个线程调用countDown()导致计数器归零时,主线程解除阻塞,继续执行后续代码。
典型应用场景:系统启动检查
最典型的应用场景是应用启动前的依赖检查。例如,Web容器启动时,可能需要等待数据库连接池、缓存服务、消息队列等多个组件全部初始化完毕。只有当所有组件都处于“Up”状态,应用才开始接收用户请求。若缺少此类同步机制,可能导致部分请求因依赖未就绪而失败。
代码实战示例
以下代码演示了如何通过CountDownLatch模拟主线程等待4个外部应用程序(App1-App4)启动完成的过程。
1. 主线程代码
import java.util.concurrent.CountDownLatch;
public class Tester {
public static void main(String args[]) {
// 设置计数器为4,代表需要等待4个应用程序
CountDownLatch countDownLatch = new CountDownLatch(4);
// 创建并启动4个工作线程
Thread app1 = new Thread(new Application("App1", countDownLatch));
Thread app2 = new Thread(new Application("App2", countDownLatch));
Thread app3 = new Thread(new Application("App3", countDownLatch));
Thread app4 = new Thread(new Application("App4", countDownLatch));
// 启动所有线程进行异步检查
app1.start();
app2.start();
app3.start();
app4.start();
try {
// 主线程在此阻塞,直到计数值降为0
countDownLatch.await();
System.out.println("All applications are up and running.");
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
2. 工作线程代码
每个线程负责模拟一个应用的启动过程,并在结束后通知主线程。
class Application implements Runnable {
private String name;
private CountDownLatch countDownLatch;
public Application(String name, CountDownLatch countDownLatch) {
this.name = name;
this.countDownLatch = countDownLatch;
}
public void run() {
try {
// 模拟启动耗时操作
System.out.println(name + " started. ");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " is Up and running.");
// 关键步骤:任务完成,计数器减1
countDownLatch.countDown();
}
}
3. 执行结果分析
由于线程调度的不确定性,各子线程的输出顺序可能交错,但"All applications..."这行打印语句必定在所有App都显示"is Up and running"之后出现。典型输出如下:
App2 started. App3 started. App1 started. App4 started. App1 is Up and running. App3 is Up and running. App4 is Up and running. App2 is Up and running. All applications are up and running.
注意事项与排错
在使用CountDownLatch时,需注意以下几点以避免常见错误:
- 线程中断处理:主线程调用await()期间若被中断,会抛出InterruptedException。应根据业务需求决定是传播异常还是恢复中断状态。
- 超时控制:上述示例使用了无参await()。如果担心某个子线程卡死导致主线程永远等待,建议使用await(long timeout, TimeUnit unit)方法设置超时时间,防止资源泄露。
- 单次性:切勿尝试重置CountDownLatch的计数值。如果需要重复使用类似的同步逻辑,请考虑使用CyclicBarrier或ReentrantLock配合Condition。
- 内存可见性:CountDownLatch内部使用了volatile和AQS保证状态变更的可见性,普通开发者无需手动处理synchronized或volatile修饰。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









