目录

Java 并发编程掌握:CountDownLatch 全解析

第一部分:核心概念解析 (图片内容深度拆解)

1. 什么是 CountDownLatch?

2. 那个经典的“跑步比赛”模型

第二部分:为什么需要它?(扩展知识)

1. 区别一:灵活的“完成”定义

2. 区别二:线程池友好

3. 区别三:多对多等待

第三部分:实战场景举例 (面试加分项)

场景一:服务启动自检 (Service Startup)

场景二:并发压力测试 (Load Testing)

第四部分:新手必知的坑

1. 忘记 countDown (死锁风险)

2. 是一次性的

3. await 的超时设置

第五部分:总结速记

1. Thread.join() 的做法:死等服务员下班

2. CountDownLatch 的做法:只等菜齐

图片里的“火箭检查”例子翻译:



你好!欢迎来到 Java 并发编程的“倒计时”站!

这张图片讲解的是 CountDownLatch (倒计时锁/计数器)。这是一个非常实用、非常高频的并发工具类。

  • 核心功能:让一个(或多个)线程等待,直到其他 N 个线程完成了一组操作后,才恢复执行。

  • 生活中的例子

    • 百米赛跑:裁判(主线程)必须等到 8 名运动员(子线程)全部跑过终点,才能宣布比赛结束。

    • 拼团:必须凑满 5 个人,系统才会触发发货。

    • 火箭发射:必须完成“检查引擎”、“检查燃料”、“检查气象”等 10 项检查,指挥官才能按下“点火”按钮。

为了帮你彻底吃透,我为你准备了两份超详尽的资料:

  1. CountDownLatchDemo.java:我还原了图片中的代码,并进行了修正(图片里的 Runable 拼写错了,且缺少 finally 块,我都帮你修好了)。注释极其详细,精确到每一行。

  2. 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. 那个经典的“跑步比赛”模型

图片里的代码逻辑非常清晰,我们拆解一下它的三部曲:

  1. 初始化 (Init)

    • new CountDownLatch(N)

    • 比如 N=10,就像裁判手里拿了一个计分板,上面写着 10。

  2. 子任务执行 (Task)

    • 每个子线程干完活后,调用 latch.countDown()

    • 这就像运动员跑过终点,裁判把计分板上的数字擦掉,写成 9...8...7...

    • 注意countDown() 是非阻塞的,子线程调完这行代码后,可以继续去干别的事(或者线程结束),它不关心裁判怎么想。

  3. 主线程等待 (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)

假设你写了一个电商后端系统。启动时,必须确保:

  1. 数据库连接成功。

  2. Redis 缓存预热完成。

  3. MQ 消息队列连接成功。

这三步是互不干扰的,可以并行。

  • 做法:主线程创建 latch(3)。开启 3 个线程分别去连 DB、Redis、MQ。连上了就 countDown()

  • 结果:主线程 await()。等三个都好了,系统正式对外提供服务。

场景二:并发压力测试 (Load Testing)

你想测试你的服务器能抗住多少并发。你需要模拟 “1000 个人同时点击按钮”。

如果你用 for 循环启动线程,那第 1 个线程启动时,第 1000 个可能还没生出来,无法做到“同时”。

  • 做法

    1. 创建一个 CountDownLatch(1) (作为发令枪)。

    2. 启动 1000 个线程。每个线程 run() 的第一行就是 gun.await()(都在起跑线等着)。

    3. 主线程准备好后,调用 gun.countDown()(砰!)。

    4. 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,你可以根据返回值做错误处理)。

第五部分:总结速记

  1. CountDownLatch = 倒数计数器

  2. 构造函数定 N,countDown 减 1,await 等到 0。

  3. 核心用途:等待多线程任务完成、统一触发多线程开始。

  4. 铁律countDown 必须写在 finally 里。

希望这份解释能帮你彻底搞懂这个知识点!代码可以直接复制运行,看看效果哦!


这一段话的核心其实就在讲:Thread.join() 太死板了,而 CountDownLatch 很灵活。

为了让你秒懂,我用**“去饭店吃饭”**给你举个例子。

假设你是顾客(主线程),服务员是子线程。你需要等待服务员把菜端上来,你才能开始吃。


1. Thread.join() 的做法:死等服务员下班

  • 逻辑join() 必须等待线程完全结束(代码跑完,线程销毁)。

  • 场景

    1. 服务员把菜端上桌了。

    2. 你刚想动筷子,系统拦住你:“不行!服务员还没下班呢!”

    3. 你眼睁睁看着服务员去擦桌子、洗碗、换衣服、打卡下班、走出店门。

    4. 系统:“好,服务员没了(线程结束了),你可以吃了。”

  • 槽点:菜都凉了!我只关心菜有没有到,我管服务员之后干嘛去呢?


2. CountDownLatch 的做法:只等菜齐

  • 逻辑countDown() 只是一个信号,说“这一步搞定了”,线程可以继续活着干别的事

  • 场景

    1. 服务员把菜端上桌,喊了一声:“菜齐了!”(调用 countDown())。

    2. 你立刻开始吃(主线程 await() 解除阻塞)。

    3. 服务员转头去擦别的桌子、洗碗(线程继续执行剩下的代码)。

  • 优势:只要关键任务(上菜)完成了,就不耽误你(主线程)的事,不用死等服务员下班。


图片里的“火箭检查”例子翻译:

  • 任务:发射火箭前要检查引擎。

  • join 模式:检查员检查完引擎 -> 开车回家 -> 洗澡睡觉(线程结束)。指挥官必须等检查员睡着了,才能按发射按钮。(太慢)

  • Latch 模式:检查员检查完引擎 -> 对讲机喊一句“引擎OK”(countDown) -> 指挥官立刻发射。检查员随后慢慢收拾工具回家。(这才是正常逻辑)

总结一句人话:

  • join 是等人走(线程死透)。

  • CountDownLatch 是等活干完(给个信号就行)。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐