CountDownLatch、CyclicBarrier、Semaphore 的区别?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 同步语义理解:面试官不仅仅是想知道这三个类 API 的区别,更是想知道你是否理解它们分别解决 "等待完成"(CountDownLatch)、"等待到齐"(CyclicBarrier)、"限流控制"(Semaphore)三种不同的协调语义。

  2. 底层实现差异:考察你是否知道 CountDownLatchSemaphore 基于 AQS,而 CyclicBarrier 基于 ReentrantLock + Condition,理解这个差异是区分 "会用" 和 "懂原理" 的分水岭。

  3. 场景选型能力:给你一个实际场景,能不能迅速判断该用哪个?比如 "多线程并行处理任务后汇总结果" 用 CountDownLatch,"多轮比赛所有选手准备就绪后开赛" 用 CyclicBarrier

核心答案

一句话区分:CountDownLatch 是 "等 N 个任务做完"(一次性),CyclicBarrier 是 "等 N 个线程到齐后一起走"(可复用),Semaphore 是 "限流,最多允许 N 个线程同时执行"。

对比维度 CountDownLatch CyclicBarrier Semaphore
核心语义 等待 N 个事件完成 N 个线程互相等待到齐 控制并发访问数量
计数方向 递减到 0,不可重置 递减到 0,自动重置 获取/释放许可证
可复用 ❌ 一次性 ✅ 自动重置(Cyclic ✅ 随时获取释放
底层实现 AQS(state = 剩余次数) ReentrantLock + Condition AQS(state = 许可数)
参与者角色 主线程等待,子线程 countDown 所有线程都是对等的 获取许可的线程
典型场景 并行任务汇总、多服务初始化等待 多轮并行计算、多人游戏同步 限流、连接池、资源池

深度解析

一、用生活场景秒懂三者区别

三个类比应该很直观了:

  • CountDownLatch:裁判(主线程)等选手(子线程)跑完。选手不关心别人,跑完就走了。裁判等到所有人冲线才公布成绩。一次性的,比赛结束就没了。

  • CyclicBarrier:5 个人拼车,谁先到了都在等,最后一个人到了才一起出发。人人对等,没有 "裁判" 角色。到齐后自动重置,可以等下一轮(这就是 Cyclic 的含义)。

  • Semaphore:停车场就 3 个车位,先到先停,满了就在外面等,有人开走了就放一辆进来。控制的是同时进入的数量,和 "等待完成" 无关。

二、CountDownLatch——"一次性倒计时器"

// 典型场景:主线程等待 3 个子任务完成后汇总
CountDownLatch latch = new CountDownLatch(3);

// 3 个子线程并行执行
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            // 执行任务...
            System.out.println(Thread.currentThread().getName() + " 完成");
        } finally {
            latch.countDown(); // 计数 -1,必须在 finally 里保证执行
        }
    }).start();
}

// 主线程等待
latch.await(); // 阻塞,直到 count 减到 0
System.out.println("所有任务完成,开始汇总");

底层原理:基于 AQS,state 初始化为 N,每次 countDown()state 减 1(CAS),await() 检查 state == 0,不为 0 就进 CLH 队列挂起。当 state 减到 0 时,唤醒所有等待线程。

一个坑:如果某个子线程异常了没执行到 countDown()await() 就会永远等下去。所以 countDown() 一定要放 finally 块里。

三、CyclicBarrier——"循环栅栏"

// 典型场景:多轮并行计算(如模拟赛跑预赛→复赛→决赛)
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    // 所有线程到齐后执行的回调(由最后到达的线程执行)
    System.out.println("所有选手就位,发令枪响!");
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        for (int round = 1; round <= 3; round++) {
            System.out.println(Thread.currentThread().getName() + " 第 " + round + " 轮准备就绪");
            barrier.await(); // 等其他线程到齐,到齐后自动重置
            // 所有线程同时继续执行
        }
    }).start();
}

底层原理:不基于 AQS!内部用 ReentrantLock + Condition 实现。每个线程调用 await() 时将计数减 1(count = parties - 已经 await 的线程数),如果不是最后一个,就 condition.await() 等待。最后一个线程到达时,signalAll() 唤醒所有人,并 自动重置计数,准备下一轮。

支持回调:构造函数可以传一个 Runnable,所有线程到齐后、继续执行前,会先执行这个回调。非常适合做 "阶段汇总"。

异常处理:如果某个线程在 await() 时被中断或超时,栅栏会被 "打破"(BrokenBarrierException),其他等待线程都会收到异常退出。可以调用 .reset() 手动重置。

四、Semaphore——"信号量/许可证"

// 典型场景:限制数据库连接数为 5
Semaphore semaphore = new Semaphore(5);

for (int i = 0; i < 20; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire(); // 获取许可证(剩余 -1),没有了就等
            // 访问数据库...
            System.out.println(Thread.currentThread().getName() + " 获取连接");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release(); // 释放许可证(剩余 +1),必须放 finally
        }
    }).start();
}

底层原理:基于 AQS,state 就是许可证数量。acquire() 尝试把 state 减 1,减到 0 就排队等待;release()state 加 1,唤醒等待线程。

Semaphore 还支持公平模式:

// 公平信号量(按等待顺序获取)
Semaphore fair = new Semaphore(5, true);

五、三者核心差异对比

参与者角色 来看,CountDownLatch 存在明确的 "主从关系"——主线程调用 await() 等待,子线程各自 countDown() 完成就走人,子线程之间互不关心。CyclicBarrier 则是 "人人平等",所有线程都调用 await(),谁先到了就等,缺一个都不走。Semaphore 没有等待的概念,它只关心 "当前有几个线程在用资源"。

生命周期 来看,CountDownLatch 是一次性的,计数减到 0 就废了,想再用得重新 newCyclicBarrier 到齐后自动重置计数,天然支持多轮循环。Semaphore 则随时 acquire/release,没有 "轮次" 的概念。

底层实现 来看,CountDownLatchSemaphore 都基于 AQS,通过 state 变量 + CAS 实现计数和排队。CyclicBarrier 走的是另一条路,内部用 ReentrantLock + Condition 实现等待/唤醒机制,和 AQS 无关

六、怎么选?一张决策图搞定

  • 你需要 一个线程等一堆线程完成?→ CountDownLatch
  • 你需要 一堆线程互相等齐了再一起走?→ CyclicBarrier
  • 你需要 限制同时执行的线程数量?→ Semaphore
  • 你需要 多轮重复的 "等齐再走"?→ CyclicBarrierCountDownLatch 是一次性的)

面试高频追问

  1. CountDownLatch 用完了能复用吗?

    不能。state 减到 0 就结束了,没有重置机制。如果需要重复使用,用 CyclicBarrier,或者重新 new 一个 CountDownLatch

  2. CyclicBarrierBrokenBarrierException 是怎么回事?

    如果某个线程在 await() 时被中断、超时或者调用了 reset(),栅栏会被 "打破",其他正在 await() 的线程会收到 BrokenBarrierException。这是为了防止部分线程永远等下去。处理方式是 reset() 重建栅栏,或者终止所有线程。

  3. Semaphoreacquire()tryAcquire() 有什么区别?

    acquire() 会阻塞等待直到拿到许可证;tryAcquire() 立即返回,拿不到就返回 false,不阻塞。tryAcquire 非常适合做限流——拿不到就返回 "系统繁忙",不让人在那傻等。

常见面试变体

  • "CountDownLatchCyclicBarrier 的区别?"
  • "什么场景用 Semaphore?"
  • "CountDownLatch 的底层实现原理?"
  • "CyclicBarrier 为什么能循环使用?"

记忆口诀

一句话区分

  • CountDownLatch一等多(一个主线程等 N 个子线程完成)
  • CyclicBarrier多等一齐(N 个线程互相等到齐,再一起走)
  • Semaphore限流(最多 N 个线程同时执行)

底层实现CountDownLatchSemaphore 基于 AQS,CyclicBarrier 基于 Lock + Condition

可复用CountDownLatch 一次性,CyclicBarrier 可循环,Semaphore 随时获取释放

总结

CountDownLatchCyclicBarrierSemaphore 解决的是三种不同的线程协调问题:等待完成、等待到齐、限流控制。记住核心区别——"一等多" vs "多等一齐" vs "限流",再加上底层实现的差异(AQS vs Lock + Condition),这道题基本拿满分。选型的时候别纠结,看场景对号入座就行。