深入理解 Java 并发编程:CountDownLatch 与 CyclicBarrier 源码对比与场景落地

Java 并发编程是多线程应用的核心,而 CountDownLatchCyclicBarrierjava.util.concurrent 包中两个重要的同步工具。它们都用于协调多线程任务,但设计理念和使用场景有显著差异。本文将从源码层面对比分析两者,并通过实际场景展示如何落地应用。文章结构清晰,逐步深入:先介绍基础概念,再解析源码实现,接着对比差异,最后提供实战案例。所有代码示例均原创,基于 Java 标准库。


1. 基础概念与用途

在并发编程中,线程同步是确保数据一致性和任务协调的关键。CountDownLatchCyclicBarrier 都基于计数器机制,但目标不同:

  • CountDownLatch:用于等待一组事件完成。它初始化一个计数器(如 $N$),线程调用 countDown() 减 1,主线程通过 await() 阻塞直到计数器归零。典型场景:主线程等待多个子线程任务结束。
  • CyclicBarrier:用于线程在某个点同步。它设置一个屏障值(如 $M$),所有线程调用 await() 时阻塞,直到达到 $M$ 个线程后,所有线程同时释放。支持重用,适合迭代式任务,如多阶段数据处理。

两者都简化了线程管理,但 CountDownLatch 更侧重“事件计数”,而 CyclicBarrier 强调“线程会合”。


2. CountDownLatch 源码解析

CountDownLatch 基于 AbstractQueuedSynchronizer (AQS) 实现,核心是维护一个计数器。源码关键部分:

  • 构造函数:初始化计数器值 $count$。
  • countDown() 方法:减少计数器值。当 $count$ 减至 0 时,唤醒所有等待线程。
  • await() 方法:阻塞当前线程,直到 $count = 0$。

示例代码展示基本用法:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        final int N = 3; // 计数器初始值
        CountDownLatch latch = new CountDownLatch(N);
        
        for (int i = 0; i < N; i++) {
            new Thread(() -> {
                System.out.println("线程任务执行中...");
                latch.countDown(); // 减 1
            }).start();
        }
        
        latch.await(); // 主线程阻塞,直到计数器归零
        System.out.println("所有任务完成,主线程继续");
    }
}

源码亮点:countDown() 使用 CAS 操作确保原子性,性能高;await() 利用 AQS 队列管理线程阻塞,避免忙等待。


3. CyclicBarrier 源码解析

CyclicBarrier 同样基于 AQS,但设计更复杂,支持重用和回调。源码关键点:

  • 构造函数:设置屏障值 $parties$ 和可选 Runnable 回调(当所有线程到达屏障时执行)。
  • await() 方法:线程调用后阻塞,直到达到 $parties$ 个线程。内部使用“代”(generation)概念管理重用。
  • 重置机制:当屏障被打破(如线程中断),可自动或手动重置。

示例代码:

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        final int M = 4; // 屏障值
        CyclicBarrier barrier = new CyclicBarrier(M, () -> 
            System.out.println("所有线程到达屏障,执行回调")
        );
        
        for (int i = 0; i < M; i++) {
            new Thread(() -> {
                System.out.println("线程准备就绪");
                try {
                    barrier.await(); // 阻塞直到 M 个线程到达
                    System.out.println("线程继续执行");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

源码亮点:await() 通过 Generation 类处理线程中断和重置;回调机制提升灵活性,适合多轮任务。


4. 源码对比与差异分析

从实现和功能对比,两者核心差异如下表:

特性 CountDownLatch CyclicBarrier
计数器机制 单向递减,从 $N$ 到 0 双向重置,达到 $M$ 后重置为 $M$
重用性 不可重用(计数器归零后失效) 可重用(通过重置)
线程角色 主线程等待子线程 所有线程相互等待
中断处理 简单,计数器不变 复杂,支持屏障打破和重置
回调支持 有(构造函数可设 Runnable
适用场景 事件驱动(如任务启动顺序) 同步点驱动(如数据分阶段处理)

数学视角:CountDownLatch 的计数器模型为递减序列 $count \rightarrow 0$,而 CyclicBarrier 的屏障模型为周期函数,满足 $threads_{\text{awaiting}} = parties$ 时触发事件。

源码差异根源:CountDownLatch 使用 AQS 的共享模式,而 CyclicBarrier 结合了独占模式和自定义同步器。例如,在 CyclicBarrier 中,await() 方法内部调用 dowait(),处理线程队列和代更迭,复杂度更高。


5. 场景落地与实践案例

正确选择工具能提升并发性能。以下是原创实战场景,避免常见陷阱。

场景 1:CountDownLatch 用于批量任务启动
在分布式任务调度中,主线程需等待所有子任务初始化完成再启动。

public class BatchTaskStarter {
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 5;
        CountDownLatch initLatch = new CountDownLatch(taskCount);
        ExecutorService executor = Executors.newFixedThreadPool(taskCount);
        
        for (int i = 0; i < taskCount; i++) {
            executor.submit(() -> {
                initializeTask(); // 模拟初始化
                initLatch.countDown();
            });
        }
        
        initLatch.await(); // 等待初始化完成
        System.out.println("所有任务初始化完成,开始执行主逻辑");
        executor.shutdown();
    }
    
    private static void initializeTask() {
        // 初始化代码
    }
}

优势:确保资源准备就绪,避免竞态。

场景 2:CyclicBarrier 用于多阶段数据处理
在数据流水线中,多个线程分阶段处理数据,每阶段需同步。

public class DataPipeline {
    public static void main(String[] args) {
        int workerCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(workerCount, () -> 
            System.out.println("阶段完成,进入下一轮")
        );
        
        for (int i = 0; i < workerCount; i++) {
            new Thread(() -> {
                for (int phase = 0; phase < 3; phase++) { // 3 个阶段
                    processData(phase); // 处理数据
                    barrier.await(); // 同步点
                }
            }).start();
        }
    }
    
    private static void processData(int phase) {
        // 数据处理逻辑
    }
}

优势:支持多轮迭代,减少线程创建开销。

选择指南

  • CountDownLatch:当需要“一等多”时,如启动屏障。
  • CyclicBarrier:当需要“多等多”时,如循环计算。
  • 避免误用:CountDownLatch 不可重用,若需多轮,优先选 CyclicBarrier

6. 总结

CountDownLatchCyclicBarrier 是 Java 并发库的精华,理解源码能优化高并发设计。CountDownLatch 适合事件计数场景,源码简洁高效;CyclicBarrier 适合线程同步点,源码灵活可重用。实际应用中,根据任务特性选择:前者用于一次性等待,后者用于周期性协调。通过本文的源码对比和场景落地,开发者能更自信地处理复杂并发问题,提升系统稳定性。记住,并发工具不是银弹——结合 ExecutorService 和锁机制,才能构建健壮的多线程应用。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐