CountDownLatch、CyclicBarrier、Semaphore 的区别?

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

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/

面试考察点

面试官提出这个问题,通常旨在考察以下几个层面:

  1. 对常用并发工具类的理解深度: 面试官不仅仅想知道三者的表面区别,更是想知道你是否真的理解它们各自的设计意图、核心工作机制以及要解决的 “核心问题” 是什么。
  2. 实际场景的应用能力: 你是否能在真实的并发场景中(如多任务协同、资源池管理、限流等)正确选择并应用这些工具,而不是死记硬背概念。
  3. 对底层框架的认知(高级考察点): 三者都是基于 AbstractQueuedSynchronizer (AQS) 实现的,理解这一点能体现出你对 Java 并发包底层设计的了解。
  4. 线程协作与同步思想的掌握: 能否区分 “一次性协作”、“可重用协作” 和 “资源访问控制” 这三种不同的并发模型。

核心答案

三者都是 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=5CyclicBarrier 与一个只有 4 个线程的线程池一起使用,就会出现这种问题。
    • 重要特性: 它的 reset() 方法可以强制重置屏障,但会唤醒所有等待的线程并抛出 BrokenBarrierException。屏障在破损(如线程被中断)后,需要重置或新建才能继续使用。
  • Semaphore
    • 最佳实践: 同样,acquire() 的调用应谨慎,或使用 tryAcquire() 带超时的方法,避免永久阻塞。release() 也建议放在 finally 块中。
    • 公平性: 初始化时可指定是否为公平模式。在竞争激烈且要求严格 FIFO 的场景下,公平模式可以避免线程饥饿。

总结

选择 CountDownLatchCyclicBarrier 还是 Semaphore,根本在于你需要解决哪种类型的 “协调问题”:是 一次性等待可循环的多方汇聚,还是 可控的并发资源访问。理解其设计意图和底层 AQS 机制,是正确、高效使用它们的关键。