0%

CountDownLatch

CountDownLatch 是 Java 并发工具类(位于 java.util.concurrent 包),核心作用是 让一个或多个线程等待其他线程完成一系列操作后,再继续执行。它基于「计数器」实现,本质是一种「同步屏障」。

一、核心概念

  1. 计数器初始化:创建 CountDownLatch 时,指定一个非负整数作为初始计数(代表需要等待的「任务数」或「线程数」)。
  2. 计数减少:其他线程完成任务后,调用 countDown() 方法,计数器会 原子性递减 1(线程安全,无需额外加锁)。
  3. 线程等待:需要等待的线程(通常是主线程)调用 await() 方法,会 阻塞当前线程,直到计数器变为 0。
  4. 超时等待:可选调用 await(long timeout, TimeUnit unit),若超时后计数器仍未为 0,线程会自动唤醒并返回 false

二、核心特点

  • 一次性使用:计数器递减到 0 后,CountDownLatch 就失效了,无法再次重置计数(若需重复使用,可考虑 CyclicBarrier)。
  • 非阻塞递减countDown() 方法不会阻塞调用线程,调用后立即返回,仅减少计数。
  • 多线程共享:多个线程可同时调用 countDown() 或 await(),计数器状态对所有线程可见。

三、适用场景

  1. 等待多线程完成任务:比如主线程等待多个子线程下载文件、处理数据后,再合并结果。
  2. 异步转同步:比如本文开头的「短信发送异步回调转同步」—— 主线程发起短信请求后阻塞,直到回调线程调用 countDown() 唤醒。
  3. 协调线程执行顺序:比如让多个线程同时开始执行(先让所有线程等待,计数器归 0 后同时唤醒)。

四、代码示例(基础用法)

场景:主线程等待 3 个子线程完成任务后,再执行后续逻辑

java

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 初始化计数器为 3(需等待 3 个任务完成)
CountDownLatch latch = new CountDownLatch(3);

// 2. 创建线程池(3 个线程执行任务)
ExecutorService executor = Executors.newFixedThreadPool(3);

for (int i = 1; i <= 3; i++) {
final int taskNum = i;
executor.submit(() -> {
try {
System.out.println("子线程开始执行任务 " + taskNum);
Thread.sleep(1000); // 模拟任务耗时(1 秒)
System.out.println("子线程完成任务 " + taskNum);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 3. 任务完成后,计数器递减 1(必须放在 finally,避免异常导致计数未减少)
latch.countDown();
}
});
}

System.out.println("主线程等待所有子线程完成任务...");
// 4. 主线程阻塞,直到计数器变为 0(超时时间可省略,此处用 5 秒超时)
boolean isAllDone = latch.await(5, java.util.concurrent.TimeUnit.SECONDS);

if (isAllDone) {
System.out.println("所有子线程任务完成,主线程继续执行后续逻辑!");
} else {
System.out.println("超时未完成所有任务,主线程退出!");
}

executor.shutdown(); // 关闭线程池
}
}

输出结果:

plaintext

1
2
3
4
5
6
7
8
主线程等待所有子线程完成任务...
子线程开始执行任务 1
子线程开始执行任务 2
子线程开始执行任务 3
子线程完成任务 1
子线程完成任务 2
子线程完成任务 3
所有子线程任务完成,主线程继续执行后续逻辑!

五、关键注意事项

  1. countDown() 必须执行

    若子线程任务异常终止,未调用 countDown(),计数器会一直不为 0,主线程会永久阻塞(或超时后返回)。因此,countDown() 需放在 finally 块中,确保无论任务成功 / 失败都执行。

  2. 避免计数器初始化为 0

    若初始计数为 0,await() 会立即返回,countDown() 会让计数变为负数(无实际意义),失去同步作用。

  3. 超时时间合理设置

    若业务允许任务超时,建议使用 await(long, TimeUnit),避免主线程无限阻塞导致系统资源占用。

  4. 与 CyclicBarrier 的区别

    • CountDownLatch:计数器一次性递减,适合「等待多线程完成任务」。
    • CyclicBarrier:计数器可重复重置,适合「多个线程互相等待,到达屏障后再一起继续执行」(比如多阶段任务协作)。

六、在「短信异步转同步」中的应用(回顾)

在本文开头的短信场景中:

  1. 主线程发起短信请求前,创建 CountDownLatch(1)(只需等待 1 个回调事件)。
  2. 主线程调用 await(30, TimeUnit.SECONDS) 阻塞,等待回调。
  3. 短信平台回调接口被触发后,调用 countDown(),计数器变为 0,主线程被唤醒,读取短信发送结果。
  4. 若 30 秒内未收到回调,await() 返回 false,主线程判定为「超时」。

这种方式确保了主线程能同步获取异步回调的结果,同时避免了无限阻塞。