Skip to content

CountDownLatch / CyclicBarrier / Semaphore:三个 AQS 同步工具的区别与实战

问题

CountDownLatch、CyclicBarrier、Semaphore 这三个并发工具类都出自 java.util.concurrent 包,它们都基于 AQS 实现,但从使用方式到适用场景都截然不同。很多开发者只背过它们的"定义"和"经典示例",到了真要用的时候却选错工具——比如用 CountDownLatch 做循环同步,或者用 Semaphore 当计数器用。本文从源码、工作流程、实战场景三个维度拆解这三者的本质。

核心答案

1. CountDownLatch(倒计时器)

构造时指定一个初始计数 N,一个或多个线程调用 await() 阻塞等待,直到其他线程调用 countDown() 将计数减到 0 为止。

关键特征:一次性使用,不能重置。

java
// 示例:主线程等待 3 个任务全部完成
CountDownLatch latch = new CountDownLatch(3);

// 三个工作线程
for (int i = 0; i < 3; i++) {
    int taskId = i;
    new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(taskId + 1);
            System.out.println("任务 " + taskId + " 完成");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            latch.countDown(); // 务必在 finally 中调用
        }
    }).start();
}

// 主线程等待
latch.await(); // 阻塞直到计数归零
System.out.println("所有任务完成,主线程继续");

2. CyclicBarrier(循环栅栏)

构造时指定一个参与线程数 N,每个线程执行到 await() 时阻塞,当 N 个线程都到达栅栏后,所有线程同时释放,可循环使用

关键特征:可重置(reset()),所有线程互相等待,支持可选的 barrierAction(最后一个到达的线程触发)。

java
// 示例:3 个线程分阶段并行计算,每阶段同步一次
CyclicBarrier barrier = new CyclicBarrier(3, () ->
    System.out.println("=== 阶段完成,进入下一阶段 ===")
);

for (int i = 0; i < 3; i++) {
    int threadId = i;
    new Thread(() -> {
        try {
            // 阶段 1
            System.out.println("线程 " + threadId + " 完成阶段 1");
            barrier.await(); // 等待其他线程

            // 阶段 2
            System.out.println("线程 " + threadId + " 完成阶段 2");
            barrier.await(); // 再次等待,循环使用

            System.out.println("线程 " + threadId + " 结束");
        } catch (InterruptedException | BrokenBarrierException e) {
            Thread.currentThread().interrupt();
        }
    }).start();
}

3. Semaphore(信号量)

构造时指定许可数 N,线程通过 acquire() 获取许可(没有则阻塞),release() 归还许可。本质是一个限流器,控制同时访问资源的线程数。

关键特征:许可可复用,支持公平/非公平模式,acquirerelease 必须成对出现。

java
// 示例:限流,只允许 2 个线程同时访问资源
Semaphore semaphore = new Semaphore(2, true); // 公平模式

for (int i = 0; i < 5; i++) {
    int threadId = i;
    new Thread(() -> {
        try {
            semaphore.acquire();
            System.out.println("线程 " + threadId + " 获取到许可,开始工作");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release(); // 务必在 finally 中释放
        }
    }).start();
}

三者的核心区别

维度CountDownLatchCyclicBarrierSemaphore
语义等待倒计时归零线程互相等待到齐获取/归还许可
可重用❌ 一次性✅ 可循环✅ 一直可用
谁触发外部线程 countDown()参与线程自己 await()线程自己 acquire()/release()
AQS 模式共享锁独占锁(内部用 ReentrantLock + Condition)共享锁
典型场景主线程等待子任务分阶段并行计算限流、资源池化

源码分析:AQS 层面的差异

CountDownLatch 的 AQS 实现

CountDownLatch 内部有一个继承 AQS 的内部类 Sync,将初始计数设置为 AQS 的 state

  • countDown() → 调用 AQS 的 releaseShared(1)tryReleaseShared 对 state 做 CAS 减 1,减到 0 时调用 doReleaseShared() 唤醒所有等待线程。
  • await() → 调用 AQS 的 acquireSharedInterruptibly(1)tryAcquireShared 检查 state == 0,不为 0 则入队等待。

核心逻辑:计数归零时一次唤醒所有等待者,这是典型的共享锁 release 模式。

CyclicBarrier 的生成机制

CyclicBarrier 不是直接基于 AQS,而是组合了 ReentrantLock + Condition。内部维护一个 Generation 对象来区分轮次:

  • await() 调用 dowait():获取锁,检查 count 减 1 后是否为 0。
  • 最后一个到达的线程:执行 barrierAction(如果有),然后调用 nextGeneration() 唤醒所有等待线程,重置 countgeneration
  • 非最后到达的线程:调用 Condition.await() 阻塞,等待最后一个线程唤醒。

注意reset() 会抛出 BrokenBarrierException,使用方必须处理这个异常。如果某个线程在 await() 期间被中断或超时,栅栏会进入"broken"状态,所有其他线程都会收到 BrokenBarrierException

Semaphore 的公平/非公平

Semaphore 内部也有 Sync 继承 AQS,state 表示剩余许可数:

  • acquire()acquireSharedInterruptibly(1)tryAcquireShared 尝试减少 state,非公平版直接 CAS(compareAndSetState),公平版先检查 hasQueuedPredecessors()
  • release()releaseShared(1)tryReleaseShared 对 state CAS 加 1,成功则调用 doReleaseShared() 唤醒等待线程。

非公平模式:新线程到达时,直接 CAS 抢许可,可能插队到已经在队列中等待的线程前面,吞吐更高但存在饥饿风险。公平模式:严格 FIFO,队列中有等待者就入队,不插队。

生产实战:选型指南

什么场景用 CountDownLatch

  • 主线程等待 N 个并行服务初始化完成后再接受请求
  • 批量 RPC 调用,等待所有结果返回后统一处理
  • 压测工具中,等待所有线程准备就绪后同时开始

坑点countDown() 必须在 finally 块中调用,否则异常导致计数永远不归零,所有 await() 线程永久阻塞。

什么场景用 CyclicBarrier

  • 多线程分阶段计算,每阶段各自算完再同步进入下一阶段(如 MapReduce 的 shuffle 阶段)
  • 模拟并发请求,所有线程"同时"发起请求(用 await() 对齐时间点)

坑点reset() 要谨慎使用,会抛出 BrokenBarrierExceptionawait() 需要设置超时,防止线程永久阻塞。

什么场景用 Semaphore

  • 数据库连接池限流:控制同时获取连接的线程数
  • 接口 QPS 整形:控制同时处理的请求数
  • 有限资源访问控制:如打印机、文件句柄

坑点acquirerelease 必须成对出现(finally 中 release),否则泄漏一个许可就永久少一个。获取多个许可时(acquire(N))更要小心,释放数量必须一致。

总结

三个工具的本质区别可以一句话记住:CountDownLatch 是"等结果",CyclicBarrier 是"等队友",Semaphore 是"等票"。它们都基于 AQS,但 AQS 的 state 在不同类中承载了完全不同的语义——CountDownLatch 用 state 做倒计数,Semaphore 用 state 做许可池,而 CyclicBarrier 甚至绕过了 AQS 直接用了 ReentrantLock + Condition。理解这些底层的差异,才能在面试和真实场景中选对工具、写对代码。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。