CountDownLatch 和 CyclicBarrier

在Java并发编程中,CountDownLatchCyclicBarrier 是两个非常重要的同步工具类,它们都用于协调多个线程之间的执行顺序。虽然它们的功能有些相似,但设计目的和使用场景有显著区别。

核心目标: 两者都用于协调多个线程的执行顺序,使某些线程等待其他线程完成特定操作后再继续执行。它们解决的是线程间“汇合点”的问题。


📌 一、CountDownLatch (倒计时闩锁)

✅ 核心概念

  • 一个一次性使用的同步辅助工具。
  • 它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成
  • 内部维护一个计数器 (count),该计数器在初始化时设定。线程通过调用 countDown() 方法递减计数器。等待的线程调用 await() 方法会被阻塞,直到计数器减到零。

🔁 特点

  • 不可重用:一旦计数器减为0,就不能再复位。
  • 适合“一个线程等待多个线程完成”的场景
  • 可以看作是一个一次性的屏障

✅ 关键方法

  • CountDownLatch(int count): 构造函数,指定初始计数值。
  • void await(): 使当前线程等待,直到计数器减到零(除非线程被中断)。
  • boolean await(long timeout, TimeUnit unit): 使当前线程等待,直到计数器减到零、或超过指定等待时间、或线程被中断。
  • void countDown(): 将计数器减1。如果计数器达到零,则释放所有等待的线程。
  • long getCount(): 返回当前计数值(主要用于调试和测试)。

✅ 工作原理

  1. 主线程创建 CountDownLatch 对象,指定初始计数值 N(代表需要等待完成的任务数量)。
  2. 主线程启动 N 个工作线程(或任务),并将 CountDownLatch 对象传递给它们。
  3. 每个工作线程在完成其任务后,调用 latch.countDown()
  4. 主线程(或其他需要等待的线程)在启动所有工作线程后,调用 latch.await()。此时它会阻塞。
  5. N 个工作线程都调用了 countDown(),计数器减到 0。
  6. 主线程(等待线程)从 await() 返回,继续执行后续逻辑。

✅ 典型应用场景

  • 启动服务前的依赖检查: 确保所有必需的服务(数据库连接、缓存加载、配置文件读取等)都初始化完成后再启动主应用。
  • 并行任务汇总: 将一个大任务拆分成多个子任务并行执行,主线程等待所有子任务完成后再进行结果汇总。
  • 模拟并发测试: 让多个测试线程同时开始执行某个操作(所有测试线程都 await() 在同一个 CountDownLatch 上,主线程 countDown() 一次释放所有等待线程)。
  • 游戏开始等待: 等待所有玩家准备就绪(每个玩家准备完成调用 countDown())后才开始游戏。

🧪 示例

主线程等待多个子线程完成

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 1; i <= threadCount; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在执行...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 执行完毕");
                latch.countDown(); // 计数器减一
            }, "线程-" + i).start();
        }

        System.out.println("主线程等待所有子线程完成...");
        latch.await(); // 阻塞直到计数器为0
        System.out.println("所有子线程已完成,主线程继续执行");
    }
}

火箭发射前检查

public class RocketLaunch {

    public static void main(String[] args) throws InterruptedException {
        // 定义需要等待的检查项数量
        int checkItems = 4; // 例如:燃料、引擎、导航、通信
        CountDownLatch preLaunchCheckLatch = new CountDownLatch(checkItems);

        // 创建并启动检查线程
        ExecutorService executor = Executors.newFixedThreadPool(checkItems);
        executor.execute(new CheckTask("燃料系统检查", 2000, preLaunchCheckLatch));
        executor.execute(new CheckTask("引擎系统检查", 1500, preLaunchCheckLatch));
        executor.execute(new CheckTask("导航系统检查", 3000, preLaunchCheckLatch));
        executor.execute(new CheckTask("通信系统检查", 1000, preLaunchCheckLatch));

        // 主线程(指挥中心)等待所有检查完成
        System.out.println("指挥中心:等待所有发射前检查完成...");
        preLaunchCheckLatch.await(); // 阻塞直到计数器为0
        System.out.println("指挥中心:所有检查通过!开始倒计时发射!");

        executor.shutdown(); // 关闭线程池
    }

    static class CheckTask implements Runnable {
        private final String taskName;
        private final int duration;
        private final CountDownLatch latch;

        CheckTask(String taskName, int duration, CountDownLatch latch) {
            this.taskName = taskName;
            this.duration = duration;
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                System.out.println(taskName + " 开始...");
                Thread.sleep(duration); // 模拟检查耗时
                System.out.println(taskName + " 完成!");
                latch.countDown(); // 检查完成,计数器减1
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

📌 二、CyclicBarrier (循环栅栏)

✅核心概念

  • 一个可以重复使用的同步辅助工具。
  • 它允许一组线程相互等待,直到所有线程都到达一个公共的屏障点,然后它们可以一起继续执行
  • 它支持一个可选的 Runnable 动作,在所有线程到达屏障点后,由最后一个到达的线程去执行这个屏障动作(barrier action)
  • 内部也维护一个计数器 (count),表示需要到达屏障的线程数。线程通过调用 await() 表示自己已到达屏障点,并等待其他线程。

🔁 特点

  • 可以重复使用:每次所有线程到达屏障后,计数器会自动重置。
  • 适合“多线程相互等待”的场景
  • 支持 barrier action
  • 如果某个线程中断或者超时,可能会导致整个屏障失败(抛出异常)。

✅关键方法

  • CyclicBarrier(int parties): 构造函数,指定需要相互等待的线程数量。
  • CyclicBarrier(int parties, Runnable barrierAction): 构造函数,指定线程数和当所有线程到达屏障后要执行的屏障动作(由最后一个到达屏障的线程执行)。
  • int await(): 使当前线程等待,直到所有线程都调用了此方法(除非线程被中断或屏障被重置/破坏)。返回当前线程的到达索引。
  • int await(long timeout, TimeUnit unit): 带超时的等待。
  • void reset(): 将屏障重置为其初始状态。如果任何线程正在屏障处等待,它们将抛出 BrokenBarrierException。用于处理一个线程失败的情况。

✅工作原理

  1. 创建 CyclicBarrier 对象,指定参与线程数 N 和可选的 barrierAction
  2. N 个线程各自执行自己的任务。
  3. 当每个线程完成自己阶段性的任务,需要等待其他线程时,调用 barrier.await()
  4. 调用 await() 的线程会被阻塞。
  5. 当第 N 个线程调用 await() 时:
    • 如果定义了 barrierAction,则第 N 个线程会执行它。
    • 然后,所有 N 个被阻塞的线程会被同时唤醒释放,继续执行各自后续的任务。
    • 屏障自动重置为初始状态,可以再次使用(这就是“循环”的含义)。
  6. 如果等待过程中有线程被中断、超时、或者调用 reset(),屏障会被置为破坏 (broken) 状态,所有等待的线程(包括后续调用 await() 的线程)将抛出 BrokenBarrierException

✅典型应用场景

  • 分阶段并行计算: 将计算分成多个阶段,每个阶段所有线程完成自己的工作后,在屏障处同步,确保所有线程都完成当前阶段后再一起进入下一阶段。例如,并行矩阵乘法、迭代计算。
  • 多线程数据加载与处理: 多个线程分别加载数据的一部分,全部加载完成后(在屏障处汇合),再各自或一起开始处理数据。
  • 模拟多次并发测试: 需要重复进行多轮并发测试的场景,每轮测试所有线程在开始点同步(await()),然后同时开始执行测试任务。屏障可重复使用。

🧪 示例

多个线程相互等待,每轮完成后重置

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("【所有线程已到达屏障点,开始新一轮】");
        });

        for (int i = 1; i <= threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 准备中...");
                    Thread.sleep((long)(Math.random() * 2000));
                    System.out.println(Thread.currentThread().getName() + " 到达屏障点");
                    barrier.await(); // 等待其他线程

                    System.out.println(Thread.currentThread().getName() + " 继续执行");
                    Thread.sleep((long)(Math.random() * 2000));

                    System.out.println(Thread.currentThread().getName() + " 再次准备...");
                    Thread.sleep((long)(Math.random() * 2000));
                    System.out.println(Thread.currentThread().getName() + " 第二次到达屏障点");
                    barrier.await();

                    System.out.println(Thread.currentThread().getName() + " 最终执行完毕");

                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, "线程-" + i).start();
        }
    }
}

多线程计算报表汇总

public class FinancialReport {

    public static void main(String[] args) {
        // 定义参与计算的线程数(部门数)和汇总动作
        int departments = 3;
        Runnable summaryAction = () -> System.out.println("\n所有部门数据已就绪,开始生成季度汇总报告...\n");
        CyclicBarrier reportBarrier = new CyclicBarrier(departments, summaryAction);

        // 创建并启动部门计算线程
        ExecutorService executor = Executors.newFixedThreadPool(departments);
        executor.execute(new DepartmentTask("销售部", 1800, reportBarrier));
        executor.execute(new DepartmentTask("市场部", 2200, reportBarrier));
        executor.execute(new DepartmentTask("研发部", 3000, reportBarrier));

        executor.shutdown(); // 不再接受新任务,等待已提交任务完成
    }

    static class DepartmentTask implements Runnable {
        private final String deptName;
        private final int calcTime;
        private final CyclicBarrier barrier;

        DepartmentTask(String deptName, int calcTime, CyclicBarrier barrier) {
            this.deptName = deptName;
            this.calcTime = calcTime;
            this.barrier = barrier;
        }

        @Override
        public void run() {
            try {
                // 阶段1: 计算本部门数据
                System.out.println(deptName + " 正在计算本季度数据...");
                Thread.sleep(calcTime); // 模拟计算耗时
                System.out.println(deptName + " 数据计算完成!等待其他部门...");
                barrier.await(); // 等待所有部门计算完成

                // 阶段2: 所有部门数据就绪后,屏障动作(汇总报告)由最后一个到达的线程执行
                // 然后所有线程同时继续执行这里(如果有后续任务)
                // 例如:每个线程可以开始基于汇总数据进行本地分析(如果需要)
                System.out.println(deptName + " 收到汇总报告,开始进行本地分析...");
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

📌 三、CountDownLatch vs CyclicBarrier(核心区别)

特性CountDownLatchCyclicBarrier
使用次数一次性:计数器减到0后失效,不能重置。可循环使用:计数器归零后自动重置,可再次使用。
参与者角色两类角色
1. 递减者 (countDown())
2. 等待者 (await())。
通常递减者和等待者是不同的线程组(如工作线程 vs 主线程)。
单一角色:所有线程都是参与者,都调用 await() 表示到达屏障点并相互等待。
计数器管理只能由工作线程递减 (countDown())。主线程不能增加。由参与者线程调用 await() 隐式递减
等待行为等待者 (await()) 等待计数器归零。所有参与者线程相互等待,直到所有都调用 await()
屏障动作无内置屏障动作。支持可选的 Runnable barrierAction,当所有线程到达屏障时,由最后一个到达的线程执行。
重置无法重置。提供 reset() 方法手动重置屏障(会导致等待线程抛出异常)。
异常处理相对简单。countDown() 失败可能导致主线程永远等待。更复杂。一个线程中断/超时/异常会导致屏障破坏 (BrokenBarrierException),影响所有等待线程。需要 reset() 恢复。
依赖方向等待者依赖于递减者完成任务。参与者线程相互依赖,共同到达汇合点。
典型场景关键词主从依赖启动准备一次性汇总分阶段协同并行计算同步点重复测试

📌 四、应用场景对比

✅ CountDownLatch 场景

  • 主线程等待多个子线程加载配置完成后再启动服务。
  • 并发测试中模拟多个请求同时发起。
  • 各个模块并行加载数据,主模块等待全部加载完成后汇总。

✅ CyclicBarrier 场景

  • 多个玩家同时准备游戏,每轮结束后重新开始。
  • 分布式系统中多个节点协同计算,每一轮计算完成后汇总结果。
  • 模拟比赛中的接力赛跑,每个选手必须等前一个完成才能开始。

📌 五、建议

使用场景推荐类
一个线程等待多个线程完成CountDownLatch
多个线程相互等待,多次使用CyclicBarrier
需要 barrier actionCyclicBarrier
一次性同步点CountDownLatch
多轮同步点CyclicBarrier

📌 六、常见问题

Q: CountDownLatch 和 join 的区别?

  • join() 是线程级别的等待,需要显式地对每个线程调用;
  • CountDownLatch 更灵活,可以控制多个线程完成事件,不依赖具体线程对象。

Q: 如何选择 CountDownLatch 还是 CyclicBarrier?

  • 如果是一次性同步点 → 用 CountDownLatch
  • 如果是多轮同步点且需要协作 → 用 CyclicBarrier

Q: CyclicBarrier 是否线程安全?

  • 是的,它是线程安全的,内部使用了 ReentrantLock 和 Condition 来实现同步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值