CountDownLatch是Java并发包中一个简单但极其强大的同步工具类。它的核心思想可以用现实生活中的"团队协作"场景来理解:假设你是一个项目经理,手下有5个开发人员同时进行不同模块的开发工作。你需要等待所有开发人员完成任务后才能进行系统集成测试。CountDownLatch就是帮你实现这种等待机制的利器。
从技术实现角度看,CountDownLatch内部维护了一个计数器(通过AQS实现),这个计数器在构造时初始化:
java复制// 内部基于AQS的实现
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count); // 初始化计数器
}
// ...其他实现细节
}
当线程调用countDown()方法时,实际上是通过CAS操作减少计数器值:
java复制public void countDown() {
sync.releaseShared(1); // 内部调用tryReleaseShared
}
而await()方法则会检查计数器是否为0,如果不为0则当前线程会进入等待队列:
java复制public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 内部调用tryAcquireShared
}
重要提示:CountDownLatch的计数器是单向的,只能减少不能增加,这点与CyclicBarrier不同。这也是它被称为"倒计时门闩"的原因。
虽然Thread.join()也能实现等待线程完成的功能,但CountDownLatch在以下场景具有明显优势:
典型join()实现的局限性示例:
java复制// 传统join方式
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
t1.start();
t2.start();
t1.join(); // 必须等待t1完全结束
t2.join(); // 必须等待t2完全结束
改用CountDownLatch后的改进:
java复制CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
doTaskPart1();
latch.countDown(); // 完成部分工作即可通知
doTaskPart2();
}).start();
new Thread(() -> {
doTask();
latch.countDown();
}).start();
latch.await(); // 只需等待关键操作完成
让我们完善输入示例中的Excel解析场景,展示一个更健壮的实现:
java复制public class ExcelParser {
private static final int THREAD_COUNT = Runtime.getRuntime().availableProcessors();
public void parse(File excelFile) throws InterruptedException {
Workbook workbook = WorkbookFactory.create(excelFile);
int sheetCount = workbook.getNumberOfSheets();
CountDownLatch latch = new CountDownLatch(sheetCount);
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < sheetCount; i++) {
Sheet sheet = workbook.getSheetAt(i);
executor.execute(() -> {
try {
parseSheet(sheet); // 实际解析逻辑
} finally {
latch.countDown(); // 确保无论如何都会减少计数
}
});
}
// 带超时的等待,避免死锁
if (!latch.await(5, TimeUnit.MINUTES)) {
throw new RuntimeException("解析超时");
}
executor.shutdown();
System.out.println("所有sheet解析完成");
}
private void parseSheet(Sheet sheet) {
// 具体的sheet解析逻辑
}
}
避坑指南:务必在finally块中调用countDown(),否则如果解析过程中抛出异常,主线程可能会永久阻塞。
CountDownLatch可以用于构建多阶段的任务流程:
java复制// 初始化阶段
CountDownLatch initLatch = new CountDownLatch(3);
// 处理阶段
CountDownLatch processLatch = new CountDownLatch(2);
// 服务初始化
new Thread(() -> {
initServiceA();
initLatch.countDown();
}).start();
new Thread(() -> {
initServiceB();
initLatch.countDown();
}).start();
// 等待初始化完成
initLatch.await();
// 开始处理任务
new Thread(() -> {
processBatch1();
processLatch.countDown();
}).start();
new Thread(() -> {
processBatch2();
processLatch.countDown();
}).start();
// 等待处理完成
processLatch.await();
System.out.println("所有任务处理完毕");
我们可以利用CountDownLatch实现简单的并发测试工具:
java复制public class ConcurrentTester {
public static void execute(int threadCount, Runnable task)
throws InterruptedException {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
startLatch.await(); // 等待统一开始信号
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endLatch.countDown();
}
}).start();
}
long startTime = System.nanoTime();
startLatch.countDown(); // 同时释放所有线程
endLatch.await(); // 等待所有线程完成
long duration = System.nanoTime() - startTime;
System.out.printf("并发数: %d, 耗时: %.2fms%n",
threadCount, duration / 1_000_000.0);
}
}
结合线程池使用时需要特别注意:
java复制ExecutorService executor = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
try {
doWork();
} finally {
latch.countDown(); // 必须放在finally中
}
});
}
// 正确关闭线程池的方式
executor.shutdown(); // 停止接收新任务
if (!executor.awaitTermination(1, TimeUnit.HOURS)) {
executor.shutdownNow(); // 强制终止
}
latch.await();
System.out.println("所有任务完成");
问题现象:主线程在await()处永久阻塞
原因分析:
解决方案:
java复制try {
// 业务逻辑
} finally {
latch.countDown(); // 确保无论如何都会执行
}
问题现象:计数器变为负数
原因分析:countDown()调用次数超过初始计数值
解决方案:
java复制if (latch.getCount() > 0) { // 先检查再减少
latch.countDown();
}
问题现象:高并发下性能下降
优化方案:
何时选择CountDownLatch:
何时选择CyclicBarrier:
虽然CountDownLatch的await()本身已经处理了虚假唤醒,但在复杂场景中可以这样增强:
java复制while (latch.getCount() > 0) {
latch.await();
}
添加监控逻辑帮助调试:
java复制class DebuggableLatch extends CountDownLatch {
public DebuggableLatch(int count) { super(count); }
@Override
public void countDown() {
System.out.println("Count down: " + (getCount() - 1));
super.countDown();
}
}
与CompletableFuture结合使用:
java复制CountDownLatch latch = new CountDownLatch(3);
CompletableFuture.runAsync(() -> {
task1();
latch.countDown();
});
// 其他任务...
// 转换为CompletableFuture
CompletableFuture<Void> completion = CompletableFuture.runAsync(() -> {
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
在实际项目中,我发现合理使用CountDownLatch可以极大简化多线程协作的复杂度。特别是在微服务启动顺序控制、批量任务处理等场景下,它往往能提供简单有效的解决方案。但也要注意不要滥用,对于复杂的多阶段协作,可能需要考虑使用更高级的Phaser或CompletionService。