CountDownLatch、CyclicBarrier、Semaphore 的区别?
2026年01月15日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常旨在考察以下几个层面:
- 对常用并发工具类的理解深度: 面试官不仅仅想知道三者的表面区别,更是想知道你是否真的理解它们各自的设计意图、核心工作机制以及要解决的 “核心问题” 是什么。
- 实际场景的应用能力: 你是否能在真实的并发场景中(如多任务协同、资源池管理、限流等)正确选择并应用这些工具,而不是死记硬背概念。
- 对底层框架的认知(高级考察点): 三者都是基于
AbstractQueuedSynchronizer(AQS) 实现的,理解这一点能体现出你对 Java 并发包底层设计的了解。 - 线程协作与同步思想的掌握: 能否区分 “一次性协作”、“可重用协作” 和 “资源访问控制” 这三种不同的并发模型。
核心答案
三者都是 JDK 并发包 java.util.concurrent 中用于线程协作的工具类,但设计目标和行为模式有本质区别。
| 工具类 | 核心模型/比喻 | 关键特性 | 主要用途 |
|---|---|---|---|
CountDownLatch | 一次性门闩/倒计时器 | 1. 一次性:计数器归零后无法重置。 2. 被动等待:一个或多个线程等待其他一组线程完成任务。 | 主线程等待多个前置服务初始化完成; 模拟并发测试的起跑线。 |
CyclicBarrier | 可循环使用的栅栏 | 1. 可循环使用:计数器归零后会自动重置,可重复使用。 2. 主动协同:一组线程相互等待,全部到达屏障点后,可执行一个预定义任务,然后一起继续执行。 | 分阶段任务,如多线程数据计算,需合并每阶段结果; 多线程分批处理任务。 |
Semaphore | 信号量/资源访问许可 | 1. 控制并发访问数: 基于许可(permits)的模型。 2. 可增减许可: 支持释放许可增加资源数。 3. 常用于资源池或流量控制。 | 数据库连接池限流; 限流场景(如最多 N 个线程同时访问某 API)。 |
简单来说:CountDownLatch 是 “我等你”(一等多),CyclicBarrier 是 “我们到齐了一起走”(多等多,可循环),Semaphore 是 “只有 N 个名额,先到先得”(控制流量)。
深度解析
原理/机制
- CountDownLatch: 内部维护一个计数器
count。初始化时设定一个正整数。线程调用countDown()方法会将计数器减 1。调用await()的线程会被阻塞,直到计数器变为 0。 - CyclicBarrier: 内部维护一个计数器
parties和一个Runnable屏障动作(可选)。每个线程调用await()表示它已到达屏障,此时计数器减 1。当计数器为 0 时,所有线程被释放,屏障动作(如果存在)会由一个最后到达屏障的线程执行,然后计数器重置为parties,可开始下一轮循环。 - Semaphore: 内部维护一组许可(
permits)。线程调用acquire()尝试获取一个许可,如果无可用许可则阻塞;调用release()释放一个许可。可以创建公平或非公平的信号量。
重要底层共性: 它们都依赖于强大的 AQS 框架 来实现底层的线程排队、阻塞与唤醒机制。
代码示例
import java.util.concurrent.*;
// 1. CountDownLatch 示例:主线程等待3个前置任务完成
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 3; i++) {
final int taskId = i;
executor.submit(() -> {
try {
// 模拟任务执行耗时
Thread.sleep((long) (Math.random() * 1000));
System.out.println("Task " + taskId + " completed.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 任务完成,计数器减1
}
});
}
System.out.println("Main thread waiting for all tasks...");
latch.await(); // 主线程在此阻塞,直到计数器为0
System.out.println("All tasks completed. Main thread proceeds.");
executor.shutdown();
}
}
// 2. CyclicBarrier 示例:4个运动员(线程)在起跑线(屏障)集合,然后同时开跑
public class CyclicBarrierDemo {
public static void main(String[] args) {
int runnerCount = 4;
// 屏障动作:当所有运动员都准备好后,由最后一个调用 await() 的线程执行
CyclicBarrier barrier = new CyclicBarrier(runnerCount,
() -> System.out.println("All runners ready! Go!"));
ExecutorService executor = Executors.newFixedThreadPool(runnerCount);
for (int i = 1; i <= runnerCount; i++) {
final int runnerId = i;
executor.submit(() -> {
try {
System.out.println("Runner " + runnerId + " is preparing...");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("Runner " + runnerId + " is ready.");
barrier.await(); // 运动员到达起跑线等待
// 所有运动员都到达后,同时开始跑步
System.out.println("Runner " + runnerId + " starts running!");
} catch (Exception e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
最佳实践与常见误区
- CountDownLatch:
- 最佳实践: 确保
countDown()在finally块中调用,以防任务异常导致计数器无法归零,进而造成主线程永久等待。
- 最佳实践: 确保
- CyclicBarrier:
- 常见误区: 如果线程池的核心线程数小于屏障数(
parties),且没有足够的空闲线程来处理被释放的线程,可能会导致死锁。例如,一个parties=5的CyclicBarrier与一个只有 4 个线程的线程池一起使用,就会出现这种问题。 - 重要特性: 它的
reset()方法可以强制重置屏障,但会唤醒所有等待的线程并抛出BrokenBarrierException。屏障在破损(如线程被中断)后,需要重置或新建才能继续使用。
- 常见误区: 如果线程池的核心线程数小于屏障数(
- Semaphore:
- 最佳实践: 同样,
acquire()的调用应谨慎,或使用tryAcquire()带超时的方法,避免永久阻塞。release()也建议放在finally块中。 - 公平性: 初始化时可指定是否为公平模式。在竞争激烈且要求严格 FIFO 的场景下,公平模式可以避免线程饥饿。
- 最佳实践: 同样,
总结
选择 CountDownLatch、CyclicBarrier 还是 Semaphore,根本在于你需要解决哪种类型的 “协调问题”:是 一次性等待、可循环的多方汇聚,还是 可控的并发资源访问。理解其设计意图和底层 AQS 机制,是正确、高效使用它们的关键。