本文目录导读:

CountDownLatch 是 Java 中一个非常实用的并发工具类,它允许一个或多个线程等待其他线程完成一组操作后再继续执行。
它的核心思想就像一个倒计数器:初始化一个计数,每当一个线程完成了它的任务,就调用 countDown() 方法让计数器减1,等待的线程会调用 await() 方法阻塞,直到计数器变为0。
核心概念
- 初始计数:在创建
CountDownLatch对象时设定,表示需要等待完成的事件数量。 countDown():事件完成时调用,计数器减1。await():调用此方法的线程会被阻塞,直到计数器的值为0。- 一次性:计数器的值一旦降到0,就无法被重置,如果需要重置,请使用
CyclicBarrier。
典型应用场景
- 主线程等待多个子线程完成:主线程启动N个线程执行任务,然后主线程等待所有子线程完成后再继续。
- 并行处理:将一个大的任务拆分成多个子任务,让多个线程并行执行,最后等待所有子任务结果汇总。
- 模拟高并发:用
CountDownLatch控制多个线程同时开始执行某个操作。
代码示例
场景 1:主线程等待所有子线程完成
这是最常见的用法,主线程作为等待者,子线程作为完成者。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 初始化计数器为 5,表示有 5 个任务要完成
CountDownLatch latch = new CountDownLatch(5);
System.out.println("主线程开始,等待 " + latch.getCount() + " 个线程完成...");
// 2. 启动 5 个子线程
for (int i = 1; i <= 5; i++) {
new Thread(new Worker(latch, "线程-" + i)).start();
}
// 3. 主线程调用 await(),在此处阻塞,直到计数器为 0
latch.await();
// 或者带超时时间的等待:latch.await(3, TimeUnit.SECONDS);
System.out.println("主线程:所有子线程已完成,继续执行后续逻辑。");
}
}
// 工作线程,模拟执行任务
class Worker implements Runnable {
private final CountDownLatch latch;
private final String name;
public Worker(CountDownLatch latch, String name) {
this.latch = latch;
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " 开始工作...");
Thread.sleep((long) (Math.random() * 2000)); // 模拟随机耗时工作
System.out.println(name + " 工作完成。");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 4. 重要:任务完成后,调用 countDown() 使计数器减 1
// 务必放在 finally 块中,避免因异常导致计数器永远无法归零
latch.countDown();
}
}
}
输出示例:
主线程开始,等待 5 个线程完成...
线程-1 开始工作...
线程-2 开始工作...
线程-4 开始工作...
线程-3 开始工作...
线程-5 开始工作...
线程-5 工作完成。
线程-2 工作完成。
线程-4 工作完成。
线程-1 工作完成。
线程-3 工作完成。
主线程:所有子线程已完成,继续执行后续逻辑。
场景 2:模拟高并发(所有线程一起开始)
这里使用两个 CountDownLatch:
startLatch(初始计数 = 1):用于让所有工作线程在同一起跑线上等待。endLatch(初始计数 = 5):用于让主线程等待所有工作线程执行完毕。
import java.util.concurrent.CountDownLatch;
public class ConcurrentStartDemo {
public static void main(String[] args) throws InterruptedException {
int threadCount = 5;
// 用于控制所有线程同时开始的阀门
CountDownLatch startLatch = new CountDownLatch(1);
// 用于主线程等待所有子线程结束
CountDownLatch endLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
int finalI = i;
new Thread(() -> {
try {
// 1. 所有线程启动后,都阻塞在 startLatch.await() 这里
startLatch.await();
// 2. 当 startLatch 计数为 0 时,所有线程几乎同时开始执行
System.out.println("线程 " + Thread.currentThread().getName() + " 开始执行任务 " + finalI);
Thread.sleep((long) (Math.random() * 1000));
System.out.println("线程 " + Thread.currentThread().getName() + " 执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 每个线程执行完毕后,让 endLatch 减 1
endLatch.countDown();
}
}, "Thread-" + i).start();
}
System.out.println("主线程准备发令...");
Thread.sleep(2000); // 模拟准备工作
System.out.println("主线程:预备——跑!");
// 4. 将 startLatch 减到 0,所有等待的工作线程同时开始
startLatch.countDown();
// 5. 主线程等待所有工作线程结束
endLatch.await();
System.out.println("主线程:所有线程执行完毕,程序结束。");
}
}
关键注意事项
- 计数器无法重置:
CountDownLatch是一次性的,计数归零后不能再使用。 countDown()放在finally块中:确保无论任务是否抛出异常,计数器都能正确减1,避免死锁或程序永远等待。- 避免
await()线程无限等待:可以使用带超时参数的await(long timeout, TimeUnit unit),防止因子线程异常或未正确调用countDown()导致主线程永远阻塞。 getCount()实时查询:该方法可以查询当前剩余计数,但返回的值是瞬时的,在多线程环境下不具备严格的同步语义。
与 CyclicBarrier 的区别
| 特性 | CountDownLatch |
CyclicBarrier |
|---|---|---|
| 可重用性 | 一次性,计数归零后不可用 | 可循环使用(通过 reset() 或自动重置) |
| 角色 | 等待者(一个或多个线程)等待完成者(多个线程)完成任务 | 所有线程相互等待,直到所有线程都到达一个“屏障点” |
| 核心方法 | countDown()、await() |
await() |
| 计数器位置 | 在等待者(如主线程)手中 | 在工作线程中 |
| 典型场景 | 一个线程等待多个线程完成 | 多个线程相互等待,到达某个点后继续或执行屏障动作 |
一句话总结:
CountDownLatch 是 “一个人等一群人的结果”;
CyclicBarrier 是 “一群人互相等,人到齐了一起走”。