一、运动会的倒计时场景
在 Java 王国的年度运动会上,正在进行一场特别的接力赛。裁判手中拿着一个神奇的发令器,这个发令器有个特殊功能:可以设置倒计时数字,当所有运动员准备就绪后,发令器倒计时到 0 时才会鸣枪,所有运动员才能起跑。
这个发令器的工作原理和 Java 中的CountDownLatch如出一辙:
-
初始化时设置倒计时数字(计数器)
-
每个运动员准备好后,向发令器报告(countDown)
-
所有运动员必须等待发令器倒计时到 0(await)才能起跑
java
// 运动会发令器模拟CountDownLatch
public class RaceCountDownLatch {
private final Sync sync;
// 发令器内部的同步器
private static final class Sync {
private int count;
Sync(int count) {
this.count = count;
}
// 等待发令(运动员等待)
void await() throws InterruptedException {
// 这里简化模拟AQS等待逻辑
while (count > 0) {
Thread.sleep(10); // 模拟等待
}
}
// 运动员准备好后报告
void countDown() {
count--;
System.out.println("当前倒计时: " + count);
}
// 获取当前倒计时
int getCount() {
return count;
}
}
public RaceCountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("倒计时不能为负");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.await();
}
public void countDown() {
sync.countDown();
}
public long getCount() {
return sync.getCount();
}
}
二、发令器的核心原理
2.1 初始化倒计时
裁判需要先设置发令器的倒计时数字,比如 4 名运动员参加接力赛,倒计时设为 4:
java
// 初始化发令器,4名运动员准备
RaceCountDownLatch latch = new RaceCountDownLatch(4);
这对应CountDownLatch的构造函数,内部通过 AQS 的 state 变量保存计数器:
java
public CountDownLatch(int count) {
this.sync = new Sync(count);
}
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count); // AQS的state作为计数器
}
}
2.2 运动员等待发令(await)
所有运动员必须在起跑线后等待,直到发令器倒计时到 0:
java
// 运动员等待发令
latch.await();
System.out.println("运动员开始起跑!");
await方法会阻塞线程,直到计数器变为 0,底层通过 AQS 的共享锁实现:
java
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // AQS获取共享锁
}
// AQS中等待的核心逻辑
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1; // 计数器为0时允许通过
}
2.3 运动员准备好后报告(countDown)
每名运动员准备好后,向发令器报告,计数器减 1:
java
// 运动员准备好后报告
latch.countDown();
System.out.println("运动员已准备,倒计时减1");
countDown方法通过 CAS 操作安全地减少计数器,并在计数器为 0 时唤醒所有等待线程:
java
public void countDown() {
sync.releaseShared(1); // AQS释放共享锁
}
// AQS中释放锁的核心逻辑
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc)) // CAS更新计数器
return nextc == 0; // 计数器为0时唤醒等待线程
}
}
三、接力赛实战示例
下面是完整的接力赛模拟代码,4 名运动员各自准备,当所有人准备好后,比赛开始:
java
import java.util.concurrent.CountDownLatch;
public class RelayRaceExample {
public static void main(String[] args) throws InterruptedException {
// 初始化发令器,4名运动员
CountDownLatch latch = new CountDownLatch(4);
// 创建4名运动员
for (int i = 1; i <= 4; i++) {
final int runnerNum = i;
new Thread(() -> {
try {
System.out.println("运动员" + runnerNum + "开始准备...");
Thread.sleep((long) (Math.random() * 1000)); // 模拟准备时间
System.out.println("运动员" + runnerNum + "准备完毕!");
latch.countDown(); // 报告准备完毕,倒计时减1
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
System.out.println("等待所有运动员准备...");
latch.await(); // 主线程等待,直到所有运动员准备好
System.out.println("所有运动员准备完毕,比赛开始!");
}
}
输出结果类似:
plaintext
等待所有运动员准备...
运动员3开始准备...
运动员2开始准备...
运动员1开始准备...
运动员4开始准备...
运动员2准备完毕!
运动员1准备完毕!
运动员3准备完毕!
运动员4准备完毕!
当前倒计时: 3
当前倒计时: 2
当前倒计时: 1
当前倒计时: 0
所有运动员准备完毕,比赛开始!
四、并发请求模拟:同时释放所有线程
另一个常见场景是模拟并发请求,比如测试服务器在 10 个客户端同时访问时的表现:
java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentRequestTest {
public static void main(String[] args) throws InterruptedException {
int clientCount = 10;
CountDownLatch startSignal = new CountDownLatch(1); // 开始信号
CountDownLatch endSignal = new CountDownLatch(clientCount); // 结束信号
ExecutorService executor = Executors.newFixedThreadPool(clientCount);
for (int i = 0; i < clientCount; i++) {
final int clientId = i + 1;
executor.submit(() -> {
try {
System.out.println("客户端" + clientId + "等待开始信号...");
startSignal.await(); // 等待开始信号
System.out.println("客户端" + clientId + "发起请求!");
Thread.sleep((long) (Math.random() * 1000)); // 模拟请求处理
System.out.println("客户端" + clientId + "请求处理完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endSignal.countDown(); // 报告请求完成
}
});
}
System.out.println("准备工作进行中...");
Thread.sleep(2000); // 模拟准备时间
System.out.println("释放所有客户端!");
startSignal.countDown(); // 释放所有客户端
System.out.println("等待所有请求完成...");
endSignal.await(); // 等待所有请求完成
System.out.println("所有请求处理完毕!");
executor.shutdown();
}
}
五、与其他同步工具的区别
5.1 CountDownLatch vs CyclicBarrier
| 发令器类型 | 主要区别 | 适用场景 |
|---|---|---|
| CountDownLatch(一次性发令器) | 倒计时到 0 后不能重置,用于一组线程等待其他线程完成 | 接力赛:等待所有运动员准备好后起跑 |
| CyclicBarrier(循环发令器) | 可以重复使用,所有线程到达屏障后一起继续 | 长跑比赛:每圈所有运动员到达终点后重新开始下一圈 |
5.2 CountDownLatch vs Semaphore
| 工具 | 核心功能 | 生活类比 |
|---|---|---|
| CountDownLatch | 等待倒计时到 0,控制线程执行顺序 | 发令器:等待所有运动员准备好才起跑 |
| Semaphore | 控制同时访问资源的线程数量 | 停车场闸门:限制同时进入的车辆数 |
六、CountDownLatch 的关键要点
-
计数器原理:基于 AQS 的 state 变量作为计数器,线程通过 CAS 操作安全更新
-
等待与唤醒:await 方法阻塞线程,countDown 方法减少计数器,计数器为 0 时唤醒所有等待线程
-
适用场景:
- 等待多个任务完成后继续(如数据汇总)
- 模拟并发请求(同时释放多个线程)
- 控制线程执行顺序(如初始化阶段等待所有资源加载)
-
注意事项:
-
计数器不能重置,只能用一次
-
避免计数器值设置过大导致内存浪费
-
等待线程可被中断(await 支持 InterruptedException)
-
通过运动会的比喻,我们理解了 CountDownLatch 的核心原理:它就像一个精确的发令器,通过倒计时机制协调多个线程的执行顺序,在多线程协作中扮演着 "同步指挥家" 的角色。在实际开发中,合理使用 CountDownLatch 可以有效解决线程间的协调问题,提高代码的可靠性和可读性。