Java并发编程:CountDownLatch全解析
本文深入解析Java并发编程工具CountDownLatch的核心原理与应用。文章首先通过"跑步比赛"模型形象说明其工作原理:主线程等待多个子线程完成操作后继续执行。详细对比了与Thread.join()的区别,包括更灵活的完成定义、线程池友好性和多对多等待能力。提供了服务启动自检和并发压力测试两个实战场景,并总结了新手常见的三个使用陷阱:忘记countDown导致死锁、一次性
目录
Java 并发编程掌握:CountDownLatch 全解析

你好!欢迎来到 Java 并发编程的“倒计时”站!
这张图片讲解的是 CountDownLatch (倒计时锁/计数器)。这是一个非常实用、非常高频的并发工具类。
-
核心功能:让一个(或多个)线程等待,直到其他 N 个线程完成了一组操作后,才恢复执行。
-
生活中的例子:
-
百米赛跑:裁判(主线程)必须等到 8 名运动员(子线程)全部跑过终点,才能宣布比赛结束。
-
拼团:必须凑满 5 个人,系统才会触发发货。
-
火箭发射:必须完成“检查引擎”、“检查燃料”、“检查气象”等 10 项检查,指挥官才能按下“点火”按钮。
-
为了帮你彻底吃透,我为你准备了两份超详尽的资料:
-
CountDownLatchDemo.java:我还原了图片中的代码,并进行了修正(图片里的Runable拼写错了,且缺少finally块,我都帮你修好了)。注释极其详细,精确到每一行。 -
CountDownLatchGuide.md:一份深度学习指南。包含原理、实战场景、与join的区别,以及最重要的“避坑指南”。
请先看代码,感受一下“多线程协同”的魅力!
CountDownLatchDemo.java
// ===========================================================================
// 区域一:包引入 (Import)
// ===========================================================================
// 导入 CountDownLatch 类。
// 它是 java.util.concurrent (JUC) 包下的核心工具类。
// 名字直译是“倒计时门栓”,你可以把它想象成一个“倒数计数器”。
import java.util.concurrent.CountDownLatch;
// 导入 Random 类,用于生成随机数,模拟每个任务执行时间长短不一。
import java.util.Random;
/**
* ===========================================================================
* 类说明:
* 本代码完整复现并修正了图片中的 CountDownLatch 示例。
* * * 场景模拟:
* 模拟一场“百米赛跑”。
* 有 10 个运动员 (线程) 同时起跑。
* 裁判员 (主线程) 在终点等待。
* 只有当 10 个运动员全部冲过终点 (计数器减为0) 时,
* 裁判员才能宣布“比赛结束”。
* * * 核心逻辑:
* 1. new CountDownLatch(10): 设置计数器为 10。
* 2. countDown(): 运动员跑完,计数器减 1。
* 3. await(): 裁判员阻塞等待,直到计数器变为 0。
* ===========================================================================
*/
public class CountDownLatchDemo {
// main 方法:Java 程序的入口
// throws Exception 是为了方便演示,实际开发中建议 try-catch 处理异常
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 比赛开始:所有运动员就位 ===");
// ===================================================================
// 1. 创建 CountDownLatch 实例
// ===================================================================
// 构造函数传入 10,表示我们需要等待 10 个任务完成。
// 这是一个“倒数计数器”,初始值就是 10。
int runnerCount = 10;
CountDownLatch latch = new CountDownLatch(runnerCount);
// ===================================================================
// 2. 定义运动员的任务逻辑 (Runnable)
// ===================================================================
Runnable runnerTask = new Runnable() { // 图片里写成了 Runable (少了个n),这里已修正
@Override
public void run() {
try {
// 获取当前线程名字,代表运动员编号
String name = Thread.currentThread().getName();
System.out.println("[" + name + "] 正在奋力奔跑...");
// 模拟跑步耗时
// Math.random() 生成 0.0 到 1.0 之间的随机小数
// 乘以 10000,表示跑了 0~10 秒不等
Thread.sleep((long) (Math.random() * 10000));
System.out.println(" >>> [" + name + "] 到达终点!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// =======================================================
// 3. 核心操作:计数器减一 (countDown)
// =======================================================
// 【划重点】:这个操作一定要放在 finally 块中!
// 无论运动员是正常跑完,还是摔倒了(抛异常),都必须算他结束了。
// 否则计数器永远不会归零,裁判员(主线程)就会永远死等下去。
latch.countDown();
// 每调用一次,内部计数器 -1。
// 当减到 0 时,就会唤醒在 latch.await() 等待的线程。
System.out.println(" (当前剩余未到达人数: " + latch.getCount() + ")");
}
}
};
// ===================================================================
// 4. 启动 10 个线程
// ===================================================================
for (int i = 0; i < runnerCount; i++) {
// 创建线程并启动
// 线程名字设为 "运动员-0" 到 "运动员-9"
new Thread(runnerTask, "运动员-" + i).start();
}
System.out.println("=== 裁判员正在终点等待... ===");
// ===================================================================
// 5. 主线程等待 (await)
// ===================================================================
// 这一行代码是“阻断”的。
// 主线程运行到这里会暂停(Block),直到 latch 的计数器变为 0。
// 也就是说,必须等上面 10 个线程都执行完 latch.countDown() 之后,
// 主线程才能继续往下走。
latch.await();
// ===================================================================
// 6. 比赛结束
// ===================================================================
// 能执行到这里,说明 latch 已经变成 0 了。
System.out.println("############################################");
System.out.println("所有人都到达终点,裁判宣布:比赛结束!");
System.out.println("############################################");
}
}
打开
CountDownLatchGuide.md
Java 并发编程掌握:CountDownLatch 全解析
你好!欢迎来到 CountDownLatch 的世界。
如果说 Semaphore 是控制流量的红绿灯,那么 CountDownLatch 就是比赛的发令枪(控制开始)或终点线(等待结束)。
第一部分:核心概念解析 (图片内容深度拆解)
1. 什么是 CountDownLatch?
-
Count: 计数。
-
Down: 减小。
-
Latch: 门栓/门锁。
连起来理解:一把靠倒数来解锁的门栓。
它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
2. 那个经典的“跑步比赛”模型
图片里的代码逻辑非常清晰,我们拆解一下它的三部曲:
-
初始化 (Init):
-
new CountDownLatch(N) -
比如 N=10,就像裁判手里拿了一个计分板,上面写着 10。
-
-
子任务执行 (Task):
-
每个子线程干完活后,调用
latch.countDown()。 -
这就像运动员跑过终点,裁判把计分板上的数字擦掉,写成 9...8...7...
-
注意:
countDown()是非阻塞的,子线程调完这行代码后,可以继续去干别的事(或者线程结束),它不关心裁判怎么想。
-
-
主线程等待 (Wait):
-
主线程调用
latch.await()。 -
这就像裁判站在终点线。只要计分板不为 0,裁判就不准回家(阻塞)。
-
一旦变为 0,
await()也就是“门栓”打开了,主线程继续执行后续代码。
-
第二部分:为什么需要它?(扩展知识)
你可能会问:“我用 Thread.join() 好像也能实现等待啊?”
确实,thread.join() 可以让主线程等待子线程结束。但是 CountDownLatch 比它强大得多:
1. 区别一:灵活的“完成”定义
-
join(): 必须死等子线程完全停止(方法执行完毕)。 -
CountDownLatch: 更加灵活。一个线程可以多次调用countDown(),或者在任务执行到一半时就调用countDown()说“我好了”,然后继续做其他收尾工作。-
例子:火箭点火检查。检查完引擎就可以
countDown了,不需要等检查员下班回家。
-
2. 区别二:线程池友好
-
在实际开发中,我们很少直接
new Thread(),而是用线程池。 -
线程池里的线程是复用的,不会“结束”。这时候
join()就没法用了,而CountDownLatch依然完美工作。
3. 区别三:多对多等待
-
join只能一个等一个。 -
CountDownLatch可以让 1 个线程等 10 个(如代码所示),也可以让 10 个线程等 1 个(比如所有运动员等发令枪响)。
第三部分:实战场景举例 (面试加分项)
场景一:服务启动自检 (Service Startup)
假设你写了一个电商后端系统。启动时,必须确保:
-
数据库连接成功。
-
Redis 缓存预热完成。
-
MQ 消息队列连接成功。
这三步是互不干扰的,可以并行。
-
做法:主线程创建
latch(3)。开启 3 个线程分别去连 DB、Redis、MQ。连上了就countDown()。 -
结果:主线程
await()。等三个都好了,系统正式对外提供服务。
场景二:并发压力测试 (Load Testing)
你想测试你的服务器能抗住多少并发。你需要模拟 “1000 个人同时点击按钮”。
如果你用 for 循环启动线程,那第 1 个线程启动时,第 1000 个可能还没生出来,无法做到“同时”。
-
做法:
-
创建一个
CountDownLatch(1)(作为发令枪)。 -
启动 1000 个线程。每个线程
run()的第一行就是gun.await()(都在起跑线等着)。 -
主线程准备好后,调用
gun.countDown()(砰!)。 -
1000 个线程瞬间同时被唤醒,发起请求。
-
第四部分:新手必知的坑
1. 忘记 countDown (死锁风险)
这是最常见的 Bug。
如果在 countDown() 执行之前,代码抛出了异常,导致 countDown() 没被执行,那计数器永远减不到 0,主线程就会永久死锁。
-
解决方案:一定要把
countDown()放在finally块中!(参考我提供的代码)。
2. 是一次性的
CountDownLatch 是一次性的。
一旦计数器减到 0,它就没用了。如果你想再次使用(比如下一轮比赛),你不能重置它。
-
解决方案:如果你需要重复使用,请关注另一个类似的工具类 ——
CyclicBarrier(循环栅栏)。
3. await 的超时设置
latch.await() 是死等。如果某个子线程卡死了,主线程也陪葬。
-
建议:使用
latch.await(5, TimeUnit.SECONDS)。-
意思是:我最多等 5 秒。5 秒后如果还没变为 0,我也不等了,直接往下走(会返回 false,你可以根据返回值做错误处理)。
-
第五部分:总结速记
-
CountDownLatch = 倒数计数器。
-
构造函数定 N,countDown 减 1,await 等到 0。
-
核心用途:等待多线程任务完成、统一触发多线程开始。
-
铁律:
countDown必须写在finally里。
希望这份解释能帮你彻底搞懂这个知识点!代码可以直接复制运行,看看效果哦!
这一段话的核心其实就在讲:Thread.join() 太死板了,而 CountDownLatch 很灵活。
为了让你秒懂,我用**“去饭店吃饭”**给你举个例子。
假设你是顾客(主线程),服务员是子线程。你需要等待服务员把菜端上来,你才能开始吃。
1. Thread.join() 的做法:死等服务员下班
-
逻辑:
join()必须等待线程完全结束(代码跑完,线程销毁)。 -
场景:
-
服务员把菜端上桌了。
-
你刚想动筷子,系统拦住你:“不行!服务员还没下班呢!”
-
你眼睁睁看着服务员去擦桌子、洗碗、换衣服、打卡下班、走出店门。
-
系统:“好,服务员没了(线程结束了),你可以吃了。”
-
-
槽点:菜都凉了!我只关心菜有没有到,我管服务员之后干嘛去呢?
2. CountDownLatch 的做法:只等菜齐
-
逻辑:
countDown()只是一个信号,说“这一步搞定了”,线程可以继续活着干别的事。 -
场景:
-
服务员把菜端上桌,喊了一声:“菜齐了!”(调用
countDown())。 -
你立刻开始吃(主线程
await()解除阻塞)。 -
服务员转头去擦别的桌子、洗碗(线程继续执行剩下的代码)。
-
-
优势:只要关键任务(上菜)完成了,就不耽误你(主线程)的事,不用死等服务员下班。
图片里的“火箭检查”例子翻译:
-
任务:发射火箭前要检查引擎。
-
join模式:检查员检查完引擎 -> 开车回家 -> 洗澡睡觉(线程结束)。指挥官必须等检查员睡着了,才能按发射按钮。(太慢) -
Latch模式:检查员检查完引擎 -> 对讲机喊一句“引擎OK”(countDown) -> 指挥官立刻发射。检查员随后慢慢收拾工具回家。(这才是正常逻辑)
总结一句人话:
-
join是等人走(线程死透)。 -
CountDownLatch是等活干完(给个信号就行)。
更多推荐


所有评论(0)