Day52 | J.U.C工具-CountDownLatch详解
本文深入讲解了J.U.C中的CountDownLatch同步工具。CountDownLatch通过倒计时计数器实现线程协调,允许一个或多个线程等待其他线程完成任务。核心方法包括:构造初始化计数器、countDown()减计数、await()阻塞等待计数归零。文章通过赛跑发令、任务并行计算、服务初始化、并发测试等典型场景演示其应用,并对比Thread.join(),突出其线程间任务协调的细粒度优势。
此前的文章,我们讲的都是J.U.C中的集合,本文开始我们开始学习J.U.C的另外一个模块--工具。
首先要讲的就是CountDownLatch。
在高并发场景中,我们经常需要协调多个线程的步调,比如主线程等待所有子任务完成,或模拟一组线程的并发开始。
CountDownLatch就是为了解决这些问题而生的工具。
CountDownLatch是J.U.C中的一个强大同步工具,专门用来解决一等多的场景:
一个或多个线程等待其他线程完成任务后再继续执行。
本文结合代码示例和场景,深入浅出地讲解CountDownLatch的用法、原理和注意事项。
一、什么是CountDownLatch
1.1. CountDownLatch概念
CountDownLatch是一个同步辅助类,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
他通过一个计数器实现协调,计数器初始化为某个值,每次任务完成时计数减一,当计数归零时,所有等待的线程被释放。
可以把他看成是一个一次性的倒计时门栓,每完成一个事就countDown()一次,计数减到0时,所有在await()的线程统一放行。
老规矩,举个栗子:有这么一个场景,一场赛跑,选手们站在起跑线等待发令枪响,裁判倒数3,2,1,当计数归零的时候,发令枪响,所有选手同时起跑。
countDown就是裁判,复杂倒计时。
这些选手就可以看成是等待的线程,调用await阻塞。
其他线程调用countDown()减少计数。
CountDownLatch的核心作用就是让主线程(或某些线程)等待多个子任务完成,不需要逐个调用 Thread.join()。特别适合需要协调多个线程完成固定数量任务的场景。
1.2. 关键特性
一次性:计数器归零之后,CountDownLatch不能重用。
线程安全:支持多线程并发调用countDown()。
1.3. 主要方法
CountDownLatch(int count):初始化计数器。
countDown():计数减一,表示一个任务完成。
await():阻塞当前线程,直到计数归零。
await(long timeout, TimeUnit unit):带超时的等待。
getCount():获取当前计数。
二、核心原理
CountDownLatch的工作机制可以用一个倒计时门闩来描述:
1、初始化
创建CountDownLatch对象,指定初始计数count,表示需要等待的任务数。
2、计数减少
每个任务完成之后,调用countDown(),计数减一。
3、等待
主线程调用await(),阻塞直到计数归零。
4、释放
计数归零后,所有等待的线程被唤醒,继续执行。

一起来看一个简单的示例代码,模拟主线程等待三个子线程完成任务。
package com.lazy.snail.day52;
import java.util.concurrent.CountDownLatch;
/**
* @ClassName CountDownLatchDemo
* @Description TODO
* @Author lazysnail
* @Date 2025/10/20 14:28
* @Version 1.0
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
int taskId = i;
new Thread(() -> {
System.out.println("任务 " + taskId + " 开始执行...");
try {
Thread.sleep((long) (Math.random() * 1000));
System.out.println("任务 " + taskId + " 完成!");
latch.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
System.out.println("主线程等待所有任务完成...");
latch.await();
System.out.println("所有任务完成,主线程继续执行!");
}
}
执行效果:

上面的代码如果用Thread.join()来实现的话,需要这样写:
package com.lazy.snail.day52;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName JoinDemo
* @Description TODO
* @Author lazysnail
* @Date 2025/10/20 16:09
* @Version 1.0
*/
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
// 用于存储所有子线程的引用
List<Thread> threads = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
int taskId = i;
Thread thread = new Thread(() -> {
System.out.println("任务 " + taskId + " 开始执行...");
try {
Thread.sleep((long) (Math.random() * 1000));
System.out.println("任务 " + taskId + " 完成!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads.add(thread);
thread.start();
}
System.out.println("主线程等待所有任务完成...");
// 遍历并等待所有线程执行结束
for (Thread thread : threads) {
thread.join();
}
System.out.println("所有任务完成,主线程继续执行!");
}
}
虽然效果 是一样的,但是你必须持有每个线程对象的引用。
强依赖于线程的生命周期,主线程等待的是线程的终止,而不仅仅是任务的完成,粒度很粗。
在大量线程的场景下,管理这些线程对象会比较麻烦。
当然你也可以使用ExecutorService来实现上面的功能,线程池能够很好的管理线程资源。
三、使用场景
CountDownLatch在并发编程中有广泛的应用,下面我们看三个比较典型的场景。
3.1. 任务分解与汇总
主线程需要等待多个子任务完成后再汇总结果。
比如文件分片下载,等待所有分片下载完成之后再合并;数据处理,多个线程并行处理数据块,汇总最终结果。
看一个计算一个大数组的和,10 个线程并行处理的案例:
package com.lazy.snail.day52;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
/**
* @ClassName CountDownLatchDemo2
* @Description TODO
* @Author lazysnail
* @Date 2025/10/20 16:16
* @Version 1.0
*/
public class CountDownLatchDemo2 {
public static void main(String[] args) throws InterruptedException {
int[] array = new int[100];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
int threadCount = 10;
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
AtomicLong totalSum = new AtomicLong(0);
int chunkSize = array.length / threadCount;
for (int i = 0; i < threadCount; i++) {
int start = i * chunkSize;
int end = (i == threadCount - 1) ? array.length : (i + 1) * chunkSize;
executor.submit(() -> {
try {
long sum = 0;
for (int j = start; j < end; j++) {
sum += array[j];
}
totalSum.addAndGet(sum);
System.out.println(Thread.currentThread().getName() + " 计算完成,部分和: " + sum);
} finally {
latch.countDown();
}
});
}
latch.await();
System.out.println("总和: " + totalSum.get());
executor.shutdown();
}
}
计算效果:

先创建了一个整型数组,填充一些数进去。
CountDownLatch创建一个计数为10的倒计时门闩。主线程需要等10个信号才能继续执行。
简单的用ExecutorService创建一个固定包含10个线程的线程池。
AtomicLong创建一个原子长整型变量,因为多个线程会同时修改总和,如果使用普通的long类型,会产生线程安全问题。AtomicLong 保证了在多线程环境下,addAndGet操作是安全的。
计算出每个线程需要处理的数据块大小。循环10次,创建10个独立的计算任务。
把每个计算任务提交给线程池。线程池会从他的10个线程里取出一个来执行这个任务。
每个线程在完成自己的计算任务后,都会执行finally块里的代码。
latch.countDown() 会把CountDownLatch内部的计数器减一。
同时,主线程在提交完所有任务后,会执行到这一行。await() 方法会阻塞主线程,让他在这里暂停。会一直等到 CountDownLatch的计数器被减到0为止。
当第10个线程完成任务并调用countDown()后,latch的计数器变成0。
计数器归零的瞬间,正在await()的主线程被唤醒,继续往下执行。
这个时候,可以保证totalSum已经包含了所有10个部分和。
最后关闭线程池,释放资源。
3.2. 初始化等待
有些系统在启动的时候,需要等待多个服务初始化完成。
比如,Web应用程序启动前需确保数据库、缓存和日志服务都准备好了。
我们来模拟一下服务的初始化过程:
package com.lazy.snail.day52;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @ClassName CountDownLatchDemo3
* @Description TODO
* @Author lazysnail
* @Date 2025/10/20 16:33
* @Version 1.0
*/
public class CountDownLatchDemo3 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
ExecutorService executor = Executors.newFixedThreadPool(3);
String[] services = {"数据库", "缓存", "日志"};
for (String service : services) {
executor.submit(() -> {
try {
System.out.println(service + " 初始化开始...");
Thread.sleep((long) (Math.random() * 1000));
System.out.println(service + " 初始化完成!");
latch.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
System.out.println("主线程等待所有服务初始化...");
latch.await();
System.out.println("所有服务初始化完成,系统启动!");
executor.shutdown();
}
}
效果:

这个例子的核心原理其实跟上一个例子完全一样,只是应用场景不同。
系统或服务启动。主线程(应用启动流程)等待多个前置服务(数据库、缓存等)完成初始化。
他们都使用CountDownLatch(N) 让主线程通过await() 等待,直到N个在线程池中运行的子任务各自完成了自己的工作并调用了countDown()。
只是一个场景是算数据,这个是加载服务,但背后的并发控制逻辑是完全相同的。
3.3. 并发测试
有的时候,我们会利用CountDownLatch能够控制多个线程同时开始执行这个原理,用来模拟高并发场景,比如压力测试。
package com.lazy.snail.day52;
import java.util.concurrent.CountDownLatch;
/**
* @ClassName CountDownLatchDemo4
* @Description TODO
* @Author lazysnail
* @Date 2025/10/20 16:38
* @Version 1.0
*/
public class CountDownLatchDemo4 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch startLatch = new CountDownLatch(1); // 控制同时开始
CountDownLatch endLatch = new CountDownLatch(10); // 等待所有线程完成
for (int i = 1; i <= 10; i++) {
int taskId = i;
new Thread(() -> {
try {
System.out.println("线程 " + taskId + " 准备就绪,等待开始...");
startLatch.await(); // 等待发令枪
System.out.println("线程 " + taskId + " 开始执行!");
Thread.sleep((long) (Math.random() * 500));
System.out.println("线程 " + taskId + " 完成!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endLatch.countDown();
}
}).start();
}
Thread.sleep(1000); // 模拟准备时间
System.out.println("发令枪响,所有线程开始!");
startLatch.countDown();
endLatch.await();
System.out.println("所有线程执行完毕!");
}
}
CountDownLatch(1)的计数是1,所有子线程都会调用startLatch.await()来等待这个门打开。
在门打开之前,所有线程都会被阻塞。主线程通过调用一次countDown()就能同时打开大门,释放所有等待的线程。
CountDownLatch(10)的计数是10,跟前两个例子一样,用于让主线程等待所有10个子线程完成他们的任务。
大致的一个步骤就是:
for循环启动了10个线程。每个线程启动后,马上调用 startLatch.await()。因为startLatch的计数是1,所以所有10个线程都会在这里被阻塞。
主线程在启动所有子线程后,执行Thread.sleep(1000),模拟一个准备时间。这个时候,10个子线程都已就绪并处于等待状态。
主线程调用startLatch.countDown()。startLatch的计数从1变成0。
startLatch计数归零的瞬间,所有因为startLatch.await()而阻塞的10个线程同时被唤醒,几乎在同一时刻开始执行后面的代码,实现了并发执行的效果。
每个线程执行自己的任务(sleep模拟),完成后在finally块里调用endLatch.countDown(),把终点线的计数器减一。
主线程调用endLatch.await()。这个时候主线程自己也进入了第二次等待,他在等待所有10个线程都跑完。
当最后一个线程调用endLatch.countDown() 后,endLatch的计数归零,主线程被唤醒,程序结束。
四、核心方法
其实在上面的案例中,CountDownLatch中的核心方法我们都使用到了,这里做一个简单的梳理。
CountDownLatch(int count)

简单的构造方法,count就是需要等待的任务数量,用来初始化计数器。
count必须大于0,不然就会抛出IllegalArgumentException异常。
void countDown()

计数减1,表示一个任务完成了。如果计数已经是0了,调用是无效的。
void await()

阻塞当前线程,直到计数归零。可能会抛出InterruptedException,需要处理。
boolean await(long timeout, TimeUnit unit)

带超时的等待,超时之后返回false(计数未归零)。
long getCount()

返回当前计数,主要是用来调试或监控的。
五、工具对比
J.U.C里面还有像CyclicBarrier、Semaphore这些工具,暂时还没讲到, Thread.join我们在上面已经对比过了。
下面先做一个简单的区分对比:
|
工具类 |
功能 |
是否可重用 |
底层机制 |
|---|---|---|---|
|
CountDownLatch |
等待计数归零,释放等待线程 |
否 |
AQS 共享模式 |
|
CyclicBarrier |
所有线程到达屏障点后同时继续 |
是 |
ReentrantLock + Condition |
|
Semaphore |
控制同时访问资源的线程数 |
是 |
AQS 共享模式 |
|
Thread.join() |
等待线程终止 |
否 |
JVM 线程状态管理 |
针对场景来说,CountDownLatch适合一等多的场景,上面的案例中基本都基于主线程等待多个子任务。
CyclicBarrier适合多等多的循环任务,比如多线程迭代计算。
Semaphore适合控制资源访问,比如限制数据库连接数。
join的话只适合简单等待线程终止,大多数情况只用来写一些Demo。
六、常见问题
CountDownLatch是不可重用的,计数器归零之后,不可以重置,如果要可重用的,就要考虑使用CyclicBarrier。
如果子线程异常退出可能会导致没调用countDown(),主线程就一直阻塞了。在实际代码里一般都在finally块里调用countDown()。
await()还可能抛出InterruptedException,需要捕获并处理。
虽然我们可以设置计数,但是这个初始化的计数也需要合理,太高了会导致不必要的等待,太低了有可能直接报错了。
CountDownLatch是基于AQS的共享模式的,性能还可以,但是在高并发的情况下还是要注意线程争用。
七、底层实现
CountDownLatch是基于AQS(AbstractQueuedSynchronizer)的共享模式实现的,核心逻辑:
public class CountDownLatch {
private final Sync sync;
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) { setState(count); }
int getCount() { return getState(); }
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false;
int next = c - 1;
if (compareAndSetState(c, next))
return next == 0;
}
}
}
}
可以看出AQS的state就是用来表示计数的。
countDown()实际调用的是tryReleaseShared(),通过CAS减少state,当state归零时唤醒等待线程。
await()实际调用的是tryAcquireShared(),检查state是不是为0,如果不是0就阻塞。

我们之前讲的AQS相关知识就应用上了。
结语
CountDownLatch是 J.U.C中一个简单而强大的工具,适合协调多线程任务的完成。
通过合理的计数设置和异常处理,能高效解决一等多的并发问题。
希望本文的讲解和示例能帮助你快速掌握CountDownLatch,并在实际项目中灵活运用。
下一篇预告
Day53 | J.U.C工具-CyclicBarrier详解
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多推荐



所有评论(0)