Java并发工具包中CountDownLatch与CyclieBarrier使用对比插图

Java并发工具包中CountDownLatch与CyclicBarrier使用对比:从原理到实战的深度解析

在Java并发编程的实战中,我们常常会遇到需要协调多个线程步调的场景。比如,主线程需要等待所有子线程完成初始化后才能开始工作,或者一组线程必须都到达某个“集合点”才能继续下一阶段的任务。JUC(java.util.concurrent)包为我们提供了两个强大的同步辅助类来解决这类问题:CountDownLatchCyclicBarrier。很多开发者初看觉得它们功能相似,容易混淆。今天,我就结合自己的踩坑经验,带大家深入剖析两者的核心区别、适用场景以及实战中的正确用法。

一、核心概念与原理:它们到底在做什么?

首先,我们必须从设计意图上理解它们的根本不同。

CountDownLatch(倒计时闩锁):这是一个“一次性”的同步工具。你可以把它想象成一个带有数字计数器的门闩。构造时设定一个初始计数值(比如N)。任何线程(通常是主线程)调用await()方法时,会被阻塞,直到其他线程调用countDown()方法将计数器减到0,门闩打开,所有等待的线程才能继续执行。它的核心是“一个或多个线程等待一组事件发生”。事件由countDown()触发,等待由await()执行。计数器无法重置,用完即废。

CyclicBarrier(循环屏障):这是一个“可循环使用”的同步工具。它更像一个多人会议的集合点。构造时设定一个参与线程数(比如N)。每个线程执行到屏障点(调用await())时会被阻塞,并告知屏障“我已到达”。当第N个线程到达后,屏障被“冲破”(trip),所有被阻塞的线程被同时释放,并且屏障的计数器会自动重置,可以迎接下一轮(Cyclic)的同步。它的核心是“一组线程相互等待,直到所有线程都到达某个公共屏障点”。此外,它还可以在冲破屏障时,执行一个可选的“屏障动作”(Runnable)。

二、实战代码示例:一看就懂的用法

理论说再多不如代码来得直观。我们来看两个典型的场景。

场景1:使用CountDownLatch实现主线程等待所有子线程准备完毕

模拟一个游戏服务启动场景:需要先加载地图、连接数据库、初始化网络,所有准备工作完成后,才能通知“游戏开始”。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 初始化计数器为3,代表3项准备工作
        CountDownLatch latch = new CountDownLatch(3);

        new Thread(new Worker("加载地图", 2, latch)).start();
        new Thread(new Worker("连接数据库", 1, latch)).start();
        new Thread(new Worker("初始化网络", 3, latch)).start();

        System.out.println("主线程:等待所有准备工作完成...");
        // 主线程在此等待,直到计数器归零
        latch.await();
        System.out.println("主线程:所有准备就绪,游戏开始!");
    }

    static class Worker implements Runnable {
        private final String taskName;
        private final int workTime;
        private final CountDownLatch latch;

        Worker(String taskName, int workTime, CountDownLatch latch) {
            this.taskName = taskName;
            this.workTime = workTime;
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 开始:" + taskName);
                TimeUnit.SECONDS.sleep(workTime); // 模拟耗时工作
                System.out.println(Thread.currentThread().getName() + " 完成:" + taskName);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 关键!工作完成后,计数器减1
                latch.countDown();
            }
        }
    }
}

踩坑提示:务必在finally块中调用countDown(),确保即使线程执行异常,计数器也能递减,避免主线程永远等待。

场景2:使用CyclicBarrier实现多线程分阶段计算

模拟一个分布式计算场景:多个工作线程分别计算一部分数据,所有线程计算完第一阶段后,汇总中间结果,然后同时开始第二阶段计算。

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

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // 参与线程数为3,所有线程到达屏障后,执行屏障动作(由最后一个到达的线程执行)
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("=== 所有线程已完成第一阶段,开始第二阶段 ===");
        });

        for (int i = 0; i < 3; i++) {
            new Thread(new ComputeWorker(i, barrier)).start();
        }
        // 注意:CyclicBarrier可以重复使用,这里为了演示只进行了一轮。
    }

    static class ComputeWorker implements Runnable {
        private final int id;
        private final CyclicBarrier barrier;

        ComputeWorker(int id, CyclicBarrier barrier) {
            this.id = id;
            this.barrier = barrier;
        }

        @Override
        public void run() {
            try {
                System.out.println("Worker " + id + " 开始第一阶段计算");
                Thread.sleep((long) (Math.random() * 2000)); // 模拟计算耗时
                System.out.println("Worker " + id + " 完成第一阶段,等待其他线程");
                // 到达屏障点,等待其他线程
                barrier.await();

                // 屏障冲破后,所有线程同时开始第二阶段
                System.out.println("Worker " + id + " 开始第二阶段计算");
                Thread.sleep((long) (Math.random() * 1000));
                System.out.println("Worker " + id + " 完成所有工作");

            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

实战经验CyclicBarrierawait()方法会抛出BrokenBarrierException。如果一个等待的线程被中断、超时或者屏障被重置,其他等待的线程就会收到此异常,表示屏障被“破坏”了。这在处理复杂错误时很重要。

三、核心区别与选型指南

理解了用法,我们来系统对比一下,这能帮你做出正确的技术选型。

1. 核心角色与等待对象不同

  • CountDownLatch:通常由一个或多个线程(等待者)等待一组事件(countDown调用)发生。事件和等待的线程是解耦的。
  • CyclicBarrier:是一组线程(参与者自身)相互等待,直到所有线程都到达屏障点。线程既是事件的触发者,也是等待者。

2. 可重用性不同

  • CountDownLatch一次性。计数器归零后,所有await调用会立即通过,但门闩状态无法重置。想再用,只能新建一个对象。
  • CyclicBarrier可循环使用。当屏障被冲破后,计数器会自动重置,可以立即用于下一轮的线程同步。这是“Cyclic”的含义。

3. 功能扩展性不同

  • CountDownLatch:功能纯粹,就是计数和等待。
  • CyclicBarrier:功能更丰富。它允许指定一个屏障动作(Runnable),当所有线程到达后,在释放它们之前,由最后一个到达的线程执行此动作,非常适合用于合并各线程的阶段性结果。

四、总结与选型口诀

经过上面的分析和实战,我们可以得出一个简单的选型口诀:

  • 用 CountDownLatch 当:你的场景是“一等多”或“多等多”(一个/多个线程等待多个事件完成),且这个同步过程只需要一次。典型场景:服务启动等待依赖组件初始化、测试中并发执行用例后统计结果。
  • 用 CyclicBarrier 当:你的场景是“多等多”(一组线程相互等待),并且这个同步过程需要重复发生(分阶段、多轮次)。典型场景:并行迭代计算(如遗传算法)、多线程测试(模拟并发用户同时发起请求)。

最后,再分享一个我踩过的坑:在Web服务器中,我曾用CountDownLatch来等待所有处理线程结束以优雅关机。但如果某个线程因异常未能调用countDown(),就会导致关机逻辑永远挂起。因此,异常处理资源清理的严谨性,在使用这两个工具时至关重要。希望这篇对比能帮助你在并发编程的道路上,更加得心应手。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。