在多线程编程中,我们经常会遇到这样的场景:主线程需要等待多个子线程完成各自的任务后,才能继续执行后续逻辑。比如,并行下载多个文件后合并结果、初始化多个组件后启动服务、模拟并发场景等。此时,CountDownLatch 就是解决这类问题的 "利器"。
一、什么是 CountDownLatch?
CountDownLatch 是 Java 并发包(java.util.concurrent)中的一个同步工具类,它的核心作用是让一个或多个线程等待其他线程完成指定操作后再继续执行,本质是通过 "计数器递减" 实现线程间的协调。
加深理解: 可以把 CountDownLatch 想象成一个 "倒计时门闸"。
比如你组织了一场接力赛:你是裁判(主线程),需要等3个选手(子线程)都跑完自己的赛程,才能宣布比赛结束。一开始你手里拿着标有 "3" 的牌子(计数器初始值),每个选手跑完后,你就把数字减 1(调用 countDown ())。当牌子上的数字变成 0 时,你就放下门闸(等待的线程被唤醒),宣布比赛结束(继续执行后续逻辑)。整个过程中,你(裁判)一直等着,直到所有选手都跑完 ------ 这就是 CountDownLatch 的作用:让一些线程等着,直到其他线程都完成各自的事,再一起往下走。
它的名字可以拆解为两部分:
CountDown:计数器递减,每完成一个任务,计数器减 1;Latch:"门闩",当计数器归 0 时,"门闩" 打开,等待的线程被唤醒。
二、核心原理:计数器 + 阻塞唤醒
CountDownLatch 的工作机制非常直观,主要分为 3 个步骤:
1. 初始化计数器
创建 CountDownLatch 实例时,需要传入一个非负整数 count 作为初始计数器值,这个值代表需要等待的 "任务数量" 或 "线程数量"。
arduino
// 初始化计数器为300,代表需要等待300个任务完成
CountDownLatch latch = new CountDownLatch(300);
2.计数器递减(countDown())
当某个被等待的线程完成任务后,调用 countDown() 方法,计数器值会减 1。(即使计数器已为 0,再次调用也不会报错,只是无效操作。)
java
import java.util.concurrent.CountDownLatch;
public class SimpleConcurrentDemo {
public static void main(String[] args) throws InterruptedException {
int threadNum = 5; // 模拟5个并发线程
CountDownLatch latch = new CountDownLatch(threadNum);
System.out.println("准备启动 " + threadNum + " 个并发任务");
// 启动5个异步线程
for (int i = 0; i < threadNum; i++) {
final int taskId = i;
new Thread(() -> {
try {
// 模拟任务执行(随机耗时)
Thread.sleep((long) (Math.random() * 1000));
System.out.println("任务" + taskId + "完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 任务完成,计数器-1
}
}).start();
}
// 主线程等待所有并发任务完成
latch.await();
System.out.println("所有并发任务执行完毕");
}
}
3.线程等待与唤醒(await())
需要等待的线程(如主线程)调用 await() 方法后会进入阻塞状态,直到计数器值减至 0,此时所有阻塞的线程会被同时唤醒,继续执行后续逻辑。
此外,await() 还有一个带超时参数的重载方法:
java
// 等待指定时间后,无论计数器是否为 0,线程都会唤醒
//long timeout:等待的时间长度(如 5)。
//TimeUnit unit:时间单位(如 TimeUnit.SECONDS 表示秒,TimeUnit.MILLISECONDS 表示毫秒)。
boolean await(long timeout, TimeUnit unit) throws InterruptedException;
调用该方法后,当前线程会进入阻塞状态,同时开始计时:
三、不可忽视的特性
1.计数器不可重置
一旦计数器减至 0,后续再调用 countDown() 也无法改变其状态,await() 会直接返回。因此,CountDownLatch 是一次性的,无法重复使用。
2.多线程共享等待
多个线程可以同时调用 await() 等待同一个 CountDownLatch,当计数器归 0 时,所有等待线程会被同时唤醒,适合 "多等多" 的场景。
3.非独占性
与 synchronized 或 ReentrantLock 不同,CountDownLatch 不涉及 "锁竞争",它只是单纯的等待通知机制,不会限制线程对资源的访问。
那么什么是锁竞争呢?
synchronized/ReentrantLock:控制资源访问的 "锁",它们的核心作用是解决 "资源竞争" 问题------ 当多个线程需要操作同一个共享资源(如修改一个变量、操作一个文件)时,通过 "加锁" 保证同一时间只有一个线程能访问资源,避免数据错乱。
举个例子:
一群人(线程)抢着用一台打印机(共享资源),synchronized 就像打印机上的 "锁":
- 第一个人使用时 "上锁",其他人必须排队等他用完 "解锁" 后才能使用;
- 锁的核心是 "限制访问",确保资源操作的安全性。
CountDownLatch:不限制资源访问,只负责 "等待通知",它的核心作用是协调线程执行顺序 ------ 让一些线程等待其他线程完成某些操作后再继续执行,但不会限制线程对任何资源的访问。
举个例子:
老师(主线程)让 5 个学生(子线程)各自做一道题(独立任务,不需要抢资源),然后等所有人都做完后再一起讲解。
- 学生做题时可以各自用自己的草稿纸(无资源竞争),
CountDownLatch就像老师手里的 "计数器":每个学生做完题就 "减 1",直到计数器归 0,老师才开始讲解; - 整个过程中,
CountDownLatch没有限制学生做题(不控制资源),只负责 "等所有人做完" 这个通知逻辑。
一句话总结: synchronized/ReentrantLock 像 "门卫",严格控制谁能进入资源区域;CountDownLatch 像 "发令员",不限制大家做什么,只负责在所有人准备好后喊 "开始"。