CyclicBarrier,CountDownLatch,Semaphore的实现原理,以及使用场景

131 阅读5分钟

CyclicBarrierCountDownLatchSemaphore 是 Java 中用于多线程并发控制的工具类,它们的实现原理和适用场景各不相同,以下是对这三者的详细解释:

1. CyclicBarrier(循环屏障)

实现原理:

CyclicBarrier 主要用于让一组线程在某个屏障位置相互等待,直到所有线程都到达该屏障后,才能继续执行。它的实现原理涉及以下几点:

  • 核心属性
    • 一个 parties 参数,表示参与线程的总数。
    • 一个 count 参数,表示当前等待屏障的线程数量,初始值为 parties
    • 一个 Runnable 类型的任务(可选),可以在所有线程都到达屏障后由一个线程执行。
  • 同步控制
    • CyclicBarrier 通过 ReentrantLockCondition 来管理线程的等待。当一个线程到达屏障时,会调用 await() 方法,count 减 1,然后判断 count 是否为 0。如果不为 0,该线程会阻塞,进入等待队列;如果为 0,表示所有线程都到达屏障,唤醒所有等待的线程,并执行预定义的任务(如果有)。
  • 可重复使用
    • CyclicBarrier 在屏障被突破后会重置 count,允许它被再次使用。相比于 CountDownLatch 只能使用一次,CyclicBarrier 具有重用的特点,这就是“循环”名称的由来。

使用场景:

  • 多线程的阶段性同步:比如并行处理某个任务时,多个线程需要同步到某个点后,再继续执行下一阶段。适用于多个线程之间的同步机制。
  • 任务分片和合并:比如在分布式计算中,多个线程并行处理任务,最后合并处理结果,要求所有线程完成前一个阶段才能继续进入下一个阶段。

代码示例:

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();
}
  • 输出示例

    Thread-0 到达屏障
    Thread-1 到达屏障
    Thread-2 到达屏障
    所有线程已到达屏障,执行任务
    Thread-0 开始执行后续任务
    Thread-1 开始执行后续任务
    Thread-2 开始执行后续任务
    

2. CountDownLatch(倒计时门)

实现原理:

CountDownLatch 允许一个或多个线程等待,直到其他线程完成某些操作。它通过一个计数器来实现,初始值是线程或任务的数量。每当一个线程完成任务,计数器就会递减,直到减为 0 时,所有等待线程将继续执行。

  • 核心属性
    • 一个 count 计数器,初始值是需要完成任务的线程数量。
    • 每次调用 countDown() 方法,count 减 1;当 count 变为 0 时,阻塞的线程将被唤醒。
  • 同步控制
    • CountDownLatch 底层依赖 AQS(AbstractQueuedSynchronizer)实现,利用 ReentrantLock 来进行线程间的同步和计数管理。调用 await() 方法的线程将进入等待状态,直到 count 变为 0。
  • 不可重复使用
    • CountDownLatch 是一次性的,计数器一旦变为 0,无法重置或重复使用。

使用场景:

  • 等待一组任务完成

    :当多个线程执行任务时,主线程需要等待这些任务完成后再继续执行。

    • 比如主线程需要等待多个子线程的初始化工作完成后再继续。
  • 启动准备工作:多个线程都准备好后,才统一启动某个操作,比如在并发测试中,等待所有线程准备好后统一开始测试。

代码示例:

CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + " 完成任务");
            latch.countDown();  // 计数器减 1
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();
}

try {
    latch.await();  // 主线程等待,直到计数器为 0
    System.out.println("所有任务完成,主线程继续执行");
} catch (InterruptedException e) {
    e.printStackTrace();
}
  • 输出示例

    Thread-0 完成任务
    Thread-1 完成任务
    Thread-2 完成任务
    所有任务完成,主线程继续执行
    

3. Semaphore(信号量)

实现原理:

Semaphore 主要用于控制同时访问某个资源的线程数量。它维护一个许可证(permit)的计数器,每个线程在进入时需要获得许可证,完成任务后必须释放许可证。

  • 核心属性
    • 一个计数器 permits,表示可以同时访问资源的最大线程数量。
    • 当线程请求许可证时,如果 permits 大于 0,则直接获取并继续执行;如果 permits 为 0,线程将被阻塞,直到有其他线程释放许可证。
  • 同步控制
    • Semaphore 基于 AQS 实现,它通过内部队列来控制线程的阻塞和许可的获取与释放。线程调用 acquire() 时,如果计数器大于 0,许可证就会发放,计数器减 1;调用 release() 则是将计数器加 1,并可能唤醒等待的线程。

使用场景:

  • 限流控制

    :用于限制某个资源的并发访问数量,防止资源过载。

    • 比如限制对数据库连接池的并发访问数量、限制对 API 的并发请求数。
  • 流量调控:控制多线程对共享资源的访问速率。

代码示例:

Semaphore semaphore = new Semaphore(3);  // 最大并发线程数为 3

for (int i = 0; i < 6; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire();  // 获取许可
            System.out.println(Thread.currentThread().getName() + " 获得许可,开始执行");
            Thread.sleep(2000);  // 模拟任务执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + " 释放许可");
            semaphore.release();  // 释放许可
        }
    }).start();
}
  • 输出示例

    Thread-0 获得许可,开始执行
    Thread-1 获得许可,开始执行
    Thread-2 获得许可,开始执行
    Thread-0 释放许可
    Thread-3 获得许可,开始执行
    ...
    

总结对比:

  • CyclicBarrier 适合多线程协调的阶段性同步,所有线程需要同时达到某个状态才继续执行,且可以重用。
  • CountDownLatch 适合一次性等待其他线程完成某些操作,倒计时计数器达到 0 后,阻塞的线程才会被唤醒。
  • Semaphore 用于控制并发线程的数量,限制访问共享资源的线程数,适合限流资源保护场景。