基于本文回答
0
评论

CountDownLatch(倒计时器)的工作原理是什么?其典型使用场景有哪些?

知识点图片

CountDownLatch(倒计时器)是 Java 并发包 (java.util.concurrent) 中的一个基础同步工具类。它的主要作用是允许一个或多个线程等待,直到在其他线程中执行的一组操作完成

下面详细解析其工作原理和典型使用场景。


一、 工作原理

CountDownLatch 的底层是基于 AQS (AbstractQueuedSynchronizer,抽象队列同步器) 实现的。它利用了 AQS 的共享锁机制。

1. 核心结构与状态 (State)

  • 初始化: 创建 CountDownLatch 时,必须传入一个整数 count(如 new CountDownLatch(3))。这个 count 值会被直接赋值给 AQS 的内置状态变量 state
  • 这个 state 就代表了需要倒计时的次数。

2. 核心方法

  • await() 方法(等待):
    • 当一个线程调用 await() 时,它会去检查 AQS 的 state 值。
    • 如果 state > 0,说明倒计时还没结束,该线程会被放入 AQS 的等待队列中阻塞挂起。
    • 如果 state == 0,说明倒计时已经结束,该线程会直接放行,继续执行后续代码。
  • countDown() 方法(倒数):
    • 当其他线程完成了一项任务后,调用 countDown() 方法。
    • 该方法会利用 CAS (Compare-And-Swap) 机制将 AQS 的 state 值减 1。
    • state 被减到 0 时,AQS 会自动唤醒等待队列中的所有阻塞线程(共享锁的传播唤醒机制),这些被唤醒的线程将继续执行。

3. 重要特性

  • 不可重用(一次性): 一旦 count 被减到 0,CountDownLatch 就失去了作用,无法被重置。如果需要可以循环使用的屏障,应该使用 CyclicBarrier
  • 线程安全: 底层利用 AQS 和 CAS 保证了并发环境下计数器递减的原子性和可见性。

二、 典型使用场景

CountDownLatch 的使用场景主要可以分为两类:“一等多”“多等一”

场景一:主线程等待多个子线程完成任务(一等多 / 任务汇总)

这是最常见的场景。主线程把一个大任务拆分成多个小任务,交给多个线程去执行。主线程必须等待所有小任务都执行完毕后,才能继续执行(比如汇总结果)。

  • 生活隐喻: 旅游团团长(主线程)必须等待所有游客(子线程)都上大巴车后,才能宣布发车。
  • 实际业务举例:
    • 并行数据加载: 比如打开一个大屏看板,需要同时查询用户数据、订单数据、库存数据。可以开三个线程去查,主线程用 await() 等待三个查询全部完成后,再拼装成一个 JSON 响应给前端,从而大幅降低接口响应时间。
    • 分片下载/处理: 下载一个大文件,分成 5 个线程下载不同的分片,全部下载完成后,主线程负责把 5 个分片合并成一个完整文件。

代码示例伪代码:

java
int taskCount = 3;
CountDownLatch latch = new CountDownLatch(taskCount);

for (int i = 0; i < taskCount; i++) {
    new Thread(() -> {
        try {
            // 执行具体的业务逻辑...
            System.out.println("子任务执行完成");
        } finally {
            // 务必在 finally 中调用,防止异常导致计数器无法清零
            latch.countDown(); 
        }
    }).start();
}

System.out.println("主线程等待子任务...");
latch.await(); // 主线程阻塞,直到 count 变为 0
System.out.println("所有子任务完成,主线程继续执行,进行结果汇总...");

场景二:多个子线程等待主线程发令(多等一 / 并发压测)

创建多个线程,让它们处于就绪状态,但先不执行核心逻辑,而是全部阻塞等待。当主线程发出一个指令后,所有线程同时开始执行。这种做法常用来模拟高并发情况。

  • 生活隐喻: 百米赛跑,所有运动员(子线程)在起跑线上就位,等待裁判(主线程)的“发令枪”。发令枪一响,所有人同时起跑。
  • 实际业务举例:
    • 并发压力测试: 在代码里写一个测试工具,想测试某个接口在 100 个并发请求下的表现。如果用 for 循环启动 100 个线程,由于线程创建有时间差,并非绝对的同时并发。可以使用 CountDownLatch(1),让 100 个线程启动后先 await(),主线程最后 countDown() 扣动扳机,实现真正的并发齐发。

代码示例伪代码:

java
// 发令枪,初始值为 1
CountDownLatch startingGun = new CountDownLatch(1);

for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        try {
            // 所有运动员准备就绪,等待发令枪响
            startingGun.await();
            // 发令枪响后,瞬间并发执行测试逻辑
            System.out.println(Thread.currentThread().getName() + " 开始发起请求...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

System.out.println("裁判准备中...");
Thread.sleep(1000); // 模拟准备时间
System.out.println("砰!发令枪响!");
startingGun.countDown(); // 扣动发令枪,count变为0,瞬间唤醒上述100个线程

场景三:结合使用的“多等多”(发令枪 + 终点线)

在上面赛跑的例子中,通常还会结合第二个 CountDownLatch 作为“终点线”。

  1. 裁判开枪(CountDownLatch(1)),所有运动员起跑。
  2. 裁判等待所有运动员冲线(CountDownLatch(100)),然后再宣布比赛结束。

三、 使用时的注意事项(避坑指南)

  1. 必须在 finally 块中调用 countDown() 如果子线程在执行任务时抛出未捕获的异常,且没有在 finally 中调用 countDown(),会导致 count 永远减不到 0,主线程将会永久死锁(死等)
  2. 设置超时时间: 在生产环境中,推荐使用 await(long timeout, TimeUnit unit) 代替无参的 await()。这样即使某个子线程卡死,主线程也能在超时后苏醒并进行异常处理,避免整个系统挂起。
  3. 替代方案: 在 Java 8 之后,如果是针对复杂的异步任务编排和结果聚合,通常推荐使用 CompletableFuture,它的功能更强大,语法也更现代(无需手动维护计数器)。CountDownLatch 如今多用于简单的等待同步场景或底层框架开发中。
右滑查看面试常问