它们都是 java.util.concurrent 包下用于线程协作的强大工具,但各自的设计目的和使用场景截然不同。你可以把它们想象成交通管理工具:
- CountDownLatch :像一个 发令枪。所有运动员(线程)在起跑线等待,枪响(计数器归零)后同时开始。
- CyclicBarrier :像一个 集合点。一组远足的人(线程)必须全部到达某个地点后,才能一起继续前进。
- Semaphore :像一个 票务系统。一个停车场只有有限的车位(许可),有车(线程)离开,才有新车可以进入。
下面我们进行详细的对比。
1. CountDownLatch(倒计时门闩)
核心作用 :让一个或多个线程等待另一组线程完成操作。它是一次性的,计数器不能被重置。
工作机制:
- 初始化时设定一个计数值(比如
N)。 - 任何线程可以调用
countDown()方法来减少计数。 - 其他线程调用
await()方法会阻塞,直到计数值减到零,所有等待的线程才会被释放,继续执行。
典型用法:
- 主线程等待多个子线程初始化完成后再继续执行。
- 模拟并发测试,确保所有线程都准备就绪后同时开始运行。
- 等待多个远程服务调用结束后再进行结果聚合。
代码示例:
// 主线程等待 5 个工作线程全部完成
java
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5); // 初始化计数器为5
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 完成任务");
latch.countDown(); // 计数器减1
}).start();
}
latch.await(); // 主线程在此等待,直到计数器变为0
System.out.println("所有线程已完成任务,主线程继续执行");
}
}
2. CyclicBarrier(循环屏障)
核心作用 :让一组线程互相等待,直到所有线程都到达一个公共的屏障点,然后一起继续执行。它是可循环使用的。
工作机制:
- 初始化时设定一个计数值(参与线程数
N)和一个可选的Runnable任务(屏障动作)。 - 每个线程执行到屏障点时调用
await()方法,该方法会阻塞。 - 当第
N个线程调用await()后,计数达到要求,所有被阻塞的线程会被同时唤醒继续执行 ,并且计数器自动重置到初始值,可以再次使用。 - 可选的
Runnable屏障任务会在所有线程被唤醒前,由最后一个到达屏障的线程执行。
典型用法:
- 分步计算任务,将一个大任务分成N个小任务,每个线程处理一部分,最后在屏障点合并结果。
- 多轮游戏或模拟,需要所有玩家都准备好才开始下一轮。
代码示例:
// 3个线程一起到一个屏障点集合,然后继续
java
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
// 屏障动作,由最后到达的线程执行
System.out.println("所有线程已就位,开始执行下一步任务");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 到达屏障前");
barrier.await(); // 在此等待其他线程
System.out.println(Thread.currentThread().getName() + " 冲破屏障,继续执行");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
3. Semaphore(信号量)
核心作用 :控制同时访问某个特定资源的线程数量,用于做流量控制或资源池管理。
工作机制:
- 初始化时设定一个许可数量 (
permits)。 - 线程通过
acquire()方法获取一个许可(如果还有可用许可),许可数量减1。 - 如果许可已被拿完,后续调用
acquire()的线程会被阻塞,直到有其他线程释放许可。 - 线程使用完资源后,通过
release()方法释放许可,许可数量加1,并唤醒一个等待的线程。
典型用法:
- 数据库连接池,限制同时获取连接的线程数。
- 流量控制,如限制某个接口的并发调用数。
- 控制访问特定设备的线程数(如打印机)。
代码示例:
// 一个只有3个许可的信号量,控制同时执行的线程数
java
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); // 初始化3个许可
for (int i = 0; i < 10; i++) { // 启动10个线程
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 获取许可,执行中...");
Thread.sleep(2000); // 模拟业务操作
System.out.println(Thread.currentThread().getName() + " 执行完毕,释放许可");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
}
}
总结对比表
| 特性 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 核心目的 | 一个/多个线程等待其他线程完成 | 一组线程互相等待至一个公共点 | 控制同时访问资源的线程数 |
| 计数器 | 递减(countDown()),一次性 | 递增(await()),可循环重置 | 递减(acquire())/递增(release()) |
| 可重用性 | 不可重用(计数到0后失效) | 可重用(自动重置计数器) | 可重用(许可可重复获取和释放) |
| 主要方法 | await(), countDown() |
await() |
acquire(), release() |
| 计数操作 | 由任意线程减少计数 | 由等待的线程自身增加计数 | 由需要访问资源的线程获取和释放 |
| 典型场景 | 主等子、启动发令枪 | 多步骤任务同步、多轮协作 | 资源池、流量控制、互斥锁(Permits=1) |