CountDownLatch 是 Java 并发包中一个非常实用的同步工具类,它允许一个或多个线程等待其他线程完成操作后再继续执行。这个类在我处理多线程协作问题时经常使用,特别是在需要等待多个前置任务完成的场景下。
CountDownLatch 的工作原理很简单:初始化时设定一个计数器,每当一个线程完成任务后调用 countDown() 方法使计数器减1,当计数器值为0时,等待的线程就会被唤醒继续执行。这种机制特别适合主线程需要等待多个子线程完成初始化工作后再执行的场景。
注意:CountDownLatch 的计数器只能被初始化一次,不能重置。如果需要重复使用这种机制,可以考虑使用 CyclicBarrier。
CountDownLatch 的构造方法非常简单,只需要传入一个整数作为计数器初始值:
java复制// 创建一个初始计数器为5的CountDownLatch
CountDownLatch latch = new CountDownLatch(5);
这里我通常会根据实际需要等待的线程数量来设置初始值。比如有5个并行任务需要完成,就设置为5。
countDown() 方法是 CountDownLatch 的核心方法之一,每个线程完成任务后调用它来减少计数器:
java复制public void countDown() {
sync.releaseShared(1);
}
这个方法会减少计数器的值,当计数器减到0时,所有等待的线程都会被唤醒。这个方法可以在任何线程中调用,不限于创建CountDownLatch的线程。
await() 方法用于阻塞当前线程,直到计数器减到0:
java复制public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
这个方法有几个变体:
在实际项目中,我经常用它来处理系统初始化问题。比如系统启动时需要加载多个缓存,这些缓存可以并行加载,但必须全部加载完成后才能对外提供服务:
java复制public class SystemInitializer {
private static final int INIT_TASKS = 3;
private final CountDownLatch latch = new CountDownLatch(INIT_TASKS);
public void init() {
// 并行执行初始化任务
new Thread(this::loadConfig).start();
new Thread(this::loadCache).start();
new Thread(this::initDB).start();
try {
latch.await(); // 等待所有初始化任务完成
System.out.println("系统初始化完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void loadConfig() {
try {
// 模拟耗时操作
Thread.sleep(1000);
System.out.println("配置加载完成");
} finally {
latch.countDown();
}
}
// 其他初始化方法类似...
}
另一个常见场景是并行计算,将一个大任务拆分成多个小任务并行执行,最后汇总结果:
java复制public class ParallelCalculator {
private static final int WORKER_COUNT = 4;
private final CountDownLatch latch = new CountDownLatch(WORKER_COUNT);
private final List<Worker> workers = new ArrayList<>();
public int calculate() {
// 创建并启动工作线程
for (int i = 0; i < WORKER_COUNT; i++) {
Worker worker = new Worker(i);
workers.add(worker);
new Thread(worker).start();
}
try {
latch.await(); // 等待所有工作线程完成
return workers.stream().mapToInt(Worker::getResult).sum();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return 0;
}
}
private class Worker implements Runnable {
private final int id;
private int result;
Worker(int id) { this.id = id; }
@Override
public void run() {
try {
// 模拟计算过程
Thread.sleep(1000);
result = id * 10;
System.out.println("Worker " + id + " 计算完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
}
int getResult() { return result; }
}
}
虽然不太常见,但需要注意计数器溢出的可能性。CountDownLatch 的计数器是 int 类型,如果初始值设置过大(接近 Integer.MAX_VALUE),可能会导致溢出问题。
在使用 await() 方法时,一定要处理 InterruptedException。这个异常表示当前线程在等待时被中断,通常应该恢复中断状态:
java复制try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 处理中断逻辑
}
如果忘记调用 countDown() 方法,或者某些线程在执行过程中异常终止而没有调用 countDown(),会导致计数器永远不为0,等待的线程将永远阻塞。为了避免这种情况,我通常会在 finally 块中调用 countDown():
java复制try {
// 执行任务
} finally {
latch.countDown();
}
CountDownLatch 和 CyclicBarrier 都是用于线程同步的工具,但它们有几个关键区别:
Semaphore 是控制并发线程数量的工具,与 CountDownLatch 的用途不同:
计数器初始值应该与实际需要等待的任务数量严格一致。设置过大可能导致不必要的等待,设置过小可能导致部分任务未完成就继续执行。
虽然 CountDownLatch 很实用,但并不是所有多线程场景都需要它。对于简单的线程等待,使用 Thread.join() 可能更合适;对于复杂的同步需求,可能需要考虑更高级的同步工具。
在实际项目中,我通常会结合线程池使用 CountDownLatch:
java复制ExecutorService executor = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
// 执行任务
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
这种组合方式可以更好地管理系统资源,避免创建过多线程。
CountDownLatch 的实现依赖于 AbstractQueuedSynchronizer (AQS) 框架。AQS 是 Java 并发包中很多同步工具的基础,它提供了一个 FIFO 队列来管理等待线程。
CountDownLatch 使用 AQS 的 state 字段来表示计数器值。当 state > 0 时,调用 await() 的线程会被放入等待队列;当 state = 0 时,所有等待线程会被唤醒。
CountDownLatch 使用的是 AQS 的共享模式(与互斥模式相对),这意味着多个线程可以同时获取锁(当计数器为0时),这也是为什么多个线程可以同时从 await() 方法返回。
这是最常见的问题,通常有以下几种原因:
解决方法:
在高并发场景下,CountDownLatch 可能会成为性能瓶颈,因为所有等待线程会在计数器归零时被同时唤醒,可能导致资源竞争。这种情况下可以考虑:
在分布式系统中,我们也可以用类似的模式协调多个服务。虽然不能直接使用 CountDownLatch(因为它在单个 JVM 内有效),但可以借鉴其思想:
java复制public class DistributedTaskCoordinator {
private final int totalTasks;
private final AtomicInteger completedTasks = new AtomicInteger(0);
public DistributedTaskCoordinator(int totalTasks) {
this.totalTasks = totalTasks;
}
public void taskCompleted() {
if (completedTasks.incrementAndGet() == totalTasks) {
synchronized (this) {
this.notifyAll();
}
}
}
public void waitForCompletion() throws InterruptedException {
synchronized (this) {
while (completedTasks.get() < totalTasks) {
this.wait();
}
}
}
}
在单元测试中,CountDownLatch 也非常有用,特别是测试并发代码时:
java复制@Test
public void testConcurrentAccess() throws InterruptedException {
final int threadCount = 10;
final CountDownLatch startLatch = new CountDownLatch(1);
final CountDownLatch endLatch = new CountDownLatch(threadCount);
final List<Thread> threads = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
Thread t = new Thread(() -> {
try {
startLatch.await();
// 执行测试逻辑
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endLatch.countDown();
}
});
threads.add(t);
t.start();
}
// 同时启动所有线程
startLatch.countDown();
// 等待所有线程完成
endLatch.await();
// 验证结果
// ...
}
这种模式可以精确控制多个线程的启动时机,非常适合并发测试。