☰
秒懂 CountDownLatch 与 CyclicBarrier 使用场景:2 万字深度详解
2026/10/1 17:18:10 网站建设 项目流程

1. 引言:从线程协作说起

在单线程程序中,代码从上到下依次执行,逻辑直观、顺序清晰。但进入多线程并发编程之后,事情就变得复杂起来:线程之间的执行顺序由操作系统调度决定,我们无法预知哪个线程先运行、哪个线程后结束。于是,一个经典问题摆在了开发者面前——如何让多个线程在合适的时机相互等待、相互配合,共同完成一项任务?

举几个现实中常见的例子:

  • 主线程需要等待若干个子线程全部执行完毕后,才能汇总结果继续向下执行;

  • 一场压测需要先把 100 个并发请求线程全部准备好,再在同一时刻统一“发令”开始;

  • 一个并行计算任务被拆成 8 个子任务,需要等 8 个子任务都算完,才能进入下一轮迭代。

这些需求都属于线程协作 / 线程同步的范畴。在 Java 的世界里,我们最早接触的工具是synchronized、wait()、notify()与notifyAll()。它们是基石,但直接用起来非常繁琐:要自己管理锁对象、自己判断等待条件、自己处理虚假唤醒,稍有不慎就会写出死锁或永远等待的代码。

正因如此,Java 从 1.5 版本开始,在java.util.concurrent(简称 JUC)包中提供了一系列高级同步工具类。其中,CountDownLatch与CyclicBarrier就是两个专门解决“多个线程相互等待”问题的高频工具。

很多初学者分不清它们到底有什么区别、分别适合什么场景,甚至在项目中混用、误用。本文将以 2 万字的篇幅,从概念、API、底层原理、源码剖析、典型场景、实战案例到常见误区,一次性把这两个类讲透,让你真正“秒懂”它们的使用场景。

阅读建议:本文默认读者具备基础的 Java 语法与多线程概念。全文代码均可直接复制运行,建议边读边动手验证。

2. 并发同步工具类的全景图

在深入两个主角之前,先建立一个全局视角,搞清楚它们在整个 JUC 同步工具体系中的位置。

2.1 JUC 中的常用同步工具

Java 并发包中的同步工具大体可以按下表归类:

工具类核心作用是否可复用主要等待方向
CountDownLatch一个或多个线程等待其他线程完成一组操作不可复用等待方等待“计数器归零”
CyclicBarrier一组线程互相等待,到达同一个屏障点可循环复用多方互等,齐了再一起走
Semaphore控制同时访问资源的线程数量可复用获取“许可证”
Exchanger两个线程在汇合点交换数据可复用两方互等并交换
Phaser更灵活的阶段性屏障,可动态增减参与方可复用分阶段多方互等

从表中能看出:CountDownLatch的特点是“单向等待、一次性”;CyclicBarrier的特点是“多方互等、可循环”。这是二者最本质的区别,也是理解后续所有细节的钥匙。

2.2 为什么需要这两个工具

理论上,用wait()/notifyAll()也能实现等待。但原生方案至少有三个痛点:

  • 状态管理复杂:你需要手动维护一个“已完成线程数”变量,并在多线程环境下保证其可见性与原子性;

  • 条件判断易错:等待方必须在循环中判断条件,否则会遭遇虚假唤醒;

  • 通知粒度难控:notify()随机唤醒一个线程,notifyAll()又可能唤醒所有线程,难以精确表达“等 N 个都完成”的语义。

CountDownLatch和CyclicBarrier在内部封装了这些复杂性,并借助AbstractQueuedSynchronizer(AQS)和ReentrantLock等底层机制,提供了简洁、安全、高性能的等待语义。接下来,我们分别深入它们。

3. CountDownLatch 深度解析

3.1 什么是 CountDownLatch

CountDownLatch,中文常译为“倒计时门闩”或“计数门闩”。它的核心语义可以用一句话概括:

一个或多个线程阻塞等待,直到其他线程执行的一组操作全部完成,计数器递减到 0,所有等待线程被释放。

你可以把它想象成一场赛跑比赛的发令流程:先有 5 名裁判各自就位,每有一位裁判就位,计分板上的数字就减 1。运动员(等待线程)一直在起跑线上等待,当计分板归零,所有运动员同时获得出发资格。这里的“计分板”就是CountDownLatch内部维护的计数器。

3.2 核心 API

CountDownLatch的公开 API 非常精简,一共只有三个常用方法:

方法说明
CountDownLatch(int count)构造方法,初始化计数器。count 必须大于等于 0
void await()使当前线程阻塞等待,直到计数器归零或线程被中断
boolean await(long timeout, TimeUnit unit)带超时的等待,超时返回 false,计数器归零返回 true
void countDown()将计数器减 1;当计数器减到 0 时,释放所有等待线程
long getCount()返回当前计数器的值(多用于调试或日志)

先看一个最简单的示例,建立直观印象:

java

import java.util.concurrent.CountDownLatch; public class LatchBasicDemo { public static void main(String[] args) throws InterruptedException { int workerCount = 3; CountDownLatch latch = new CountDownLatch(workerCount); for (int i = 0; i < workerCount; i++) { final int no = i; new Thread(() -> { System.out.println("工人 " + no + " 开始干活"); try { Thread.sleep((no + 1) * 1000L); // 模拟不同耗时的工作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("工人 " + no + " 干完了"); latch.countDown(); // 干完一件事,计数器减 1 }).start(); } System.out.println("主线程等待所有工人干完..."); latch.await(); // 阻塞直到计数器归零 System.out.println("所有工人都干完了,主线程继续执行收尾工作"); } }

运行结果大致如下(线程打印顺序可能不同,但最后一行一定在三个“干完了”之后):

text

工人 0 开始干活 工人 1 开始干活 工人 2 开始干活 主线程等待所有工人干完... 工人 0 干完了 工人 1 干完了 工人 2 干完了 所有工人都干完了,主线程继续执行收尾工作

这个例子展示了CountDownLatch最经典的“主线程等子线程”用法。

3.3 底层原理:AQS 共享模式

CountDownLatch的实现非常优雅,整个类只依赖一个内部类Sync,而Sync继承自 AQS(AbstractQueuedSynchronizer)。AQS 是 JUC 包的核心框架,它维护了一个state字段和一个 FIFO 等待队列,并支持两种资源共享模式:

  • 独占模式(Exclusive):同一时刻只允许一个线程持有锁,如ReentrantLock;

  • 共享模式(Shared):同一时刻允许多个线程同时获取,如Semaphore、CountDownLatch。

CountDownLatch使用了共享模式。它把构造时传入的计数 count 保存在 AQS 的state中,并重写了两个关键方法:

java

protected int tryAcquireShared(int acquires) { // state 为 0 表示“门闩已打开”,获取成功;否则返回 -1 表示需要等待 return (getState() == 0) ? 1 : -1; } protected boolean tryReleaseShared(int releases) { // 循环执行 CAS 递减 state,直到 state 为 0 或递减到 0 for (;;) { int c = getState(); if (c == 0) { return false; // 已经是 0,没有可释放的 } int nextc = c - 1; if (compareAndSetState(c, nextc)) { return nextc == 0; // 减到 0 才真正唤醒所有等待线程 } } }

整个流程可以这样理解:

  • 调用await()的线程,本质是尝试以共享模式获取资源。只要state不为 0,tryAcquireShared就返回负数,该线程被包装成节点加入 AQS 等待队列并阻塞;

  • 调用countDown()的线程,本质是释放共享资源。每次用 CAS 把state减 1;当某次减完恰好变成 0 时,tryReleaseShared返回 true,AQS 会唤醒等待队列中的所有线程,这些线程再依次去重新尝试获取资源,此时state == 0,全部获取成功。

这里有两个非常值得注意的细节:

第一,计数器不能重置。AQS 的state一旦归零,就永远不会再变回正数。因为tryReleaseShared里明确判断了c == 0直接返回 false,CountDownLatch也没有提供任何把 state 重新设大的方法。这就是它“一次性”的根本原因。

第二,唤醒是“全体唤醒”。当 state 减到 0 时,AQS 会在共享模式下按传播机制唤醒队列中所有等待节点,而不是只唤醒一个。这与“多个线程同时 await”的场景完美契合。

3.4 中断与超时的处理

await()方法是可以响应中断的。如果等待线程在阻塞期间被调用了interrupt(),它会抛出InterruptedException并退出等待。因此在实际编码中,要么显式捕获处理,要么让异常向上抛出,并注意不要吞掉中断状态。

如果希望“最多等一段时间”,可以使用带超时的版本:

java

CountDownLatch latch = new CountDownLatch(2); boolean completed = latch.await(3, TimeUnit.SECONDS); if (completed) { System.out.println("两个任务都在 3 秒内完成了"); } else { System.out.println("等待超时,不再继续阻塞"); }

超时版本在生产环境中非常实用:与其让线程无限期挂起,不如设置一个合理的兜底时间,配合降级策略处理,避免因为某个子任务卡死而导致整个系统不可用。

3.5 典型使用场景

CountDownLatch的适用场景可以归纳为四类:等待子任务完成、并发启动、单点汇总、以及超时保护。

3.5.1 场景一:主线程等待子任务完成

这是CountDownLatch最常见的用法。比如你需要调用 10 个互不依赖的外部接口获取数据,然后把 10 份数据汇总。串行调用需要累加 10 次网络耗时,而并行调用则只需要等待最慢的那一次。

java

import java.util.List; import java.util.concurrent.*; public class ParallelFetchDemo { public static void main(String[] args) throws InterruptedException { int taskCount = 10; ExecutorService pool = Executors.newFixedThreadPool(10); CountDownLatch doneSignal = new CountDownLatch(taskCount); ConcurrentLinkedQueue<String> results = new ConcurrentLinkedQueue<>(); for (int i = 0; i < taskCount; i++) { final int id = i; pool.execute(() -> { try { String data = fetchRemoteData(id); // 模拟远程调用 results.add(data); } finally { doneSignal.countDown(); // 无论成功失败都要递减,避免主线程永久等待 } }); } doneSignal.await(); pool.shutdown(); System.out.println("共拿到 " + results.size() + " 条数据,开始汇总"); } private static String fetchRemoteData(int id) { try { Thread.sleep(ThreadLocalRandom.current().nextInt(500, 1500)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "data-" + id; } }

这段代码有两个值得强调的工程细节:

  • 使用线程池而不是每次都new Thread,避免线程频繁创建销毁的开销;

  • countDown()放在finally块中,保证即使远程调用抛出异常,计数器也能正常递减,主线程不会永远卡在await()上。

3.5.2 场景二:让所有线程同时开始

CountDownLatch不仅能“等别人干完”,还能反过来“让所有人一起开始”。这在模拟高并发、性能压测中非常常用。思路是用一个计数为 1 的CountDownLatch作为“发令枪”:所有工作线程先await(),主线程一声countDown(),大家同时起跑。

java

import java.util.Collections; import java.util.Set; import java.util.concurrent.*; public class ConcurrentStartDemo { public static void main(String[] args) throws InterruptedException { int threadCount = 100; ExecutorService pool = Executors.newFixedThreadPool(threadCount); CountDownLatch startGate = new CountDownLatch(1); // 发令枪 CountDownLatch endGate = new CountDownLatch(threadCount); // 全部完成信号 Set<Long> finishTimes = ConcurrentHashMap.newKeySet(); for (int i = 0; i < threadCount; i++) { pool.execute(() -> { try { startGate.await(); // 等发令 long finish = doWork(); // 要压测的目标动作 finishTimes.add(finish); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { endGate.countDown(); } }); } long begin = System.nanoTime(); startGate.countDown(); // 100 个线程同时起跑 endGate.await(); // 等待所有线程都完成 long end = System.nanoTime(); pool.shutdown(); System.out.println("100 个线程全部完成,总耗时:" + (end - begin) / 1_000_000.0 + " ms"); System.out.println("结束时间样本数:" + finishTimes.size()); } private static long doWork() { try { Thread.sleep(ThreadLocalRandom.current().nextInt(100, 300)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return System.nanoTime(); } }

这段代码使用了两把门闩,形成一个非常经典的「一放一收」结构:

  • startGate:计数为 1,所有工作线程先阻塞在startGate.await(),主线程调用一次countDown()后,100 个线程同时进入doWork();

  • endGate:计数为 100,每个线程执行完都在finally中countDown(),主线程等待其归零后再统计总耗时。

这里的startGate.countDown()就是最关键的发令动作:它的存在让「准备线程」和「正式执行」两个阶段被严格分离,可以最大程度模拟真实高并发场景下同时到达的流量。

3.5.3 场景三:等待全部服务就绪后再开放流量

在微服务架构中,网关往往需要等若干下游服务都完成初始化、注册成功之后,才开始对外接收流量。如果每来一个请求都临时判断某个服务是否就绪,逻辑就会非常复杂。此时用CountDownLatch做「单点汇总」非常合适:每个服务启动完成后执行countDown(),网关统一await(),全部就绪后一次性放行。

java

import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ServiceReadyDemo { private static final int SERVICE_COUNT = 5; private static final CountDownLatch allReady = new CountDownLatch(SERVICE_COUNT); public static void main(String[] args) throws InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(SERVICE_COUNT); for (int i = 1; i <= SERVICE_COUNT; i++) { final int serviceNo = i; pool.execute(() -> { initService(serviceNo); System.out.println("服务 " + serviceNo + " 就绪"); allReady.countDown(); // 每个服务就绪后递减一次 }); } System.out.println("网关等待所有服务就绪..."); allReady.await(); // 全部就绪后继续 System.out.println("全部服务就绪,开始对外提供流量"); pool.shutdown(); } private static void initService(int no) { try { Thread.sleep(no * 300L); // 模拟不同服务的启动耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }

注意这种场景的重点在于「汇总点唯一」:无论服务内部先后顺序如何,只要总数凑齐,网关就能继续。主线程不需要轮询每个服务的状态,也不需要维护额外的布尔变量,代码可读性和健壮性都更好。

3.5.4 场景四:带超时的兜底保护

前面几种场景都默认任务一定能完成。但在真实生产环境中,子任务可能因为网络抖动、死循环或资源耗尽而迟迟不返回。如果主线程一直await(),整个链路都会被拖垮。因此更稳妥的做法是使用带超时时间的await(),超时后执行降级逻辑。

java

import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class TimeoutProtectDemo { public static void main(String[] args) throws InterruptedException { int taskCount = 10; CountDownLatch latch = new CountDownLatch(taskCount); for (int i = 0; i < taskCount; i++) { new Thread(() -> { try { Thread.sleep(5000); // 模拟可能卡住的任务 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }).start(); } boolean finished = latch.await(2, TimeUnit.SECONDS); if (finished) { System.out.println("所有任务在 2 秒内完成"); } else { System.out.println("等待超时,执行降级:先返回部分可用数据"); } } }

执行这段代码时,任务需要 5 秒才能完成,但主线程最多等 2 秒。因此await(2, TimeUnit.SECONDS)会返回 false,程序进入降级分支。这样即使部分任务卡死,系统也能继续对外提供有限服务,而不是完全不可用。

至此,CountDownLatch的四种典型场景已经覆盖完毕。总结它们的共性就是在某个关键点设置「计数闸门」,然后由不同线程按下开关。但CountDownLatch也存在一个明显限制:不可复用。如果想让一组线程反复在多个屏障点汇合,就需要引入另一位主角——CyclicBarrier。

4. CyclicBarrier 深度解析

4.1 什么是 CyclicBarrier

CyclicBarrier,中文常译为「循环屏障」或「栅栏」。它的语义同样可以用一句话概括:

让一组线程互相等待,直到所有线程都到达同一个屏障点后,再一起继续向后执行;该屏障可以循环使用。

如果说CountDownLatch像「发令枪」或者「终点线」,那么CyclicBarrier更像「团建集合点」:所有人到齐,拍个照,然后一起进入下一个环节。每次人都到齐,集合点又能继续用于下一轮集合,因此它能循环使用。

4.2 核心 API

CyclicBarrier的 API 也不复杂,常用方法如下表:

方法说明
CyclicBarrier(int parties)构造屏障,指定需要 parties 个线程到达后才放行
CyclicBarrier(int parties, Runnable barrierAction)所有线程到达后,先执行 barrierAction,再统一放行
int await()到达屏障并等待,所有线程到达后返回本线程的到达序号
int await(long timeout, TimeUnit unit)带超时的屏障等待
int getParties()返回需要通过的线程数量
int getNumberWaiting()返回当前正在屏障处等待的线程数量
boolean isBroken()判断屏障是否已经损坏
void reset()将屏障重置到初始状态

先看一个简单示例:

java

import java.util.concurrent.CyclicBarrier; public class BarrierBasicDemo { public static void main(String[] args) { int threadCount = 4; CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> System.out.println("4 名选手都准备好了,开始这一轮!")); for (int i = 0; i < threadCount; i++) { final int no = i; new Thread(() -> { try { System.out.println("选手 " + no + " 到达第一轮屏障"); barrier.await(); // 等待其他 3 名选手 System.out.println("选手 " + no + " 通过第一轮屏障"); System.out.println("选手 " + no + " 到达第二轮屏障"); barrier.await(); // 第二轮继续使用同一个屏障 System.out.println("选手 " + no + " 通过第二轮屏障"); } catch (Exception e) { e.printStackTrace(); } }).start(); } } }

运行这段代码,你会发现同一个barrier对象被连续使用了两次,这正是循环屏障和一次性门闩最直观的差异。

4.3 底层原理:ReentrantLock 与 Condition

CyclicBarrier并没有像CountDownLatch那样直接基于 AQS 的共享模式,而是基于ReentrantLock和Condition自己实现等待队列。其核心字段大致如下:

  • lock:ReentrantLock,用于保护屏障的内部状态;

  • trip:Condition,用于阻塞和唤醒等待线程;

  • parties:每轮需要到达的线程数;

  • barrierCommand:所有线程到齐后要执行的动作;

  • generation:当前「代」对象,用于标记屏障是否被破坏;

  • count:当前这一轮尚未到达的线程数量。

简化后的await()流程如下:

java

public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // 无超时版本不会发生,实际源码类似 } } private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; lock.lock(); try { final Generation g = generation; // 1. 如果当前代已经被破坏,立即抛出异常 if (g.broken) { throw new BrokenBarrierException(); } // 2. 响应中断,中断当前线程并破坏屏障 if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); } // 3. 计算到达序号 int index = --count; // 4. 如果是最后一个到达 if (index == 0) { boolean ranAction = false; try { final Runnable command = barrierCommand; if (command != null) { command.run(); // 先执行屏障动作 } ranAction = true; nextGeneration(); // 唤醒所有等待线程,进入下一代 return 0; } finally { if (!ranAction) { breakBarrier(); // 动作执行异常时破坏屏障 } } } // 5. 不是最后一个到达,则进入条件队列等待 for (;;) { try { if (!timed) { trip.await(); } else if (nanos > 0L) { nanos = trip.awaitNanos(nanos); } } catch (InterruptedException ie) { if (g == generation && !g.broken) { breakBarrier(); // 等待中被打断,破坏屏障 throw ie; } } if (g.broken) { throw new BrokenBarrierException(); } if (g != generation) { return index; // 已经进入下一代,正常返回 } } } finally { lock.unlock(); } }

其中有两个关键方法:

java

private void nextGeneration() { trip.signalAll(); // 唤醒所有等待线程 count = parties; // 重置计数器 generation = new Generation(); // 创建新一代 } private void breakBarrier() { generation.broken = true; // 标记当前代已破坏 count = parties; // 重置计数器 trip.signalAll(); // 唤醒所有等待线程 }

从源码可以看出,CyclicBarrier通过Generation来表示屏障的生命周期。当所有线程到齐后,nextGeneration()会唤醒全部等待线程,并把count重置为parties,同时生成一个新的Generation。这样屏障就回到了初始状态,可以继续服务下一轮等待。

4.4 循环复用的本质

CountDownLatch不能复用,是因为它的 AQS state 一旦归零就永远无法恢复。而CyclicBarrier可以复用,靠的是两点:

  1. 计数器可重置:每一轮结束时会执行count = parties,把倒计时重新拉满;

  2. 代际更替:每一轮使用不同的Generation对象标记状态,既能区分不同轮次,又能避免旧一轮的线程误入新一轮。

这里的Generation就像比赛中的「场次号」。第一场到齐后,屏障进入第二场;后续到达的线程属于第二场,与第一场互不干扰。这也是为什么CyclicBarrier适合「分阶段并行计算」类场景。

4.5 典型使用场景

4.5.1 场景一:分阶段并行计算

假设需要 3 个线程分别计算一部分数据,计算分 3 个阶段进行,且每个阶段都必须等所有线程完成上一阶段后才能开始。这种场景用CyclicBarrier表达非常自然。

java

import java.util.concurrent.CyclicBarrier; public class PhasedComputeDemo { private static final int THREADS = 3; private static final int PHASES = 3; private static final CyclicBarrier barrier = new CyclicBarrier(THREADS, () -> System.out.println("----- 本阶段所有线程计算完成,进入下一阶段 -----")); public static void main(String[] args) { for (int i = 0; i < THREADS; i++) { final int threadNo = i; new Thread(() -> { try { for (int phase = 1; phase <= PHASES; phase++) { compute(threadNo, phase); barrier.await(); // 等所有线程完成本阶段 } } catch (Exception e) { e.printStackTrace(); } }).start(); } } private static void compute(int no, int phase) { System.out.println("线程 " + no + " 正在执行第 " + phase + " 阶段"); try { Thread.sleep((no + phase) * 200L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }

运行后你会发现,每个阶段结束后都会先打印提示语,再进入下一阶段。这正是CyclicBarrier循环复用的典型表现。

4.5.2 场景二:多线程数据加载后统一处理

在数据量较大的场景中,经常需要把一份大任务拆成若干份并行处理。例如从多个分片读取数据、做初步清洗,然后等所有分片都处理完,再统一做合并。

java

import java.util.concurrent.CyclicBarrier; public class ShardingLoadDemo { private static final int SHARDS = 4; private static final CyclicBarrier barrier = new CyclicBarrier(SHARDS, () -> System.out.println("所有分片加载完成,开始合并数据")); public static void main(String[] args) { for (int i = 0; i < SHARDS; i++) { final int shardNo = i; new Thread(() -> { try { loadShard(shardNo); barrier.await(); // 等所有分片加载完成 mergeShard(shardNo); // 各线程继续后续处理 } catch (Exception e) { e.printStackTrace(); } }).start(); } } private static void loadShard(int no) { System.out.println("加载分片 " + no); try { Thread.sleep(300L + no * 100L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private static void mergeShard(int no) { System.out.println("合并分片 " + no); } }
4.5.3 场景三:多轮并发测试

在做性能压测时,经常需要模拟多轮并发请求,每一轮都要让所有线程同时开始,并且每轮之间还要等待所有线程结束。用CyclicBarrier可以很方便地实现。

java

import java.util.concurrent.CyclicBarrier; public class MultiRoundStressDemo { private static final int CONCURRENCY = 10; private static final int ROUNDS = 3; private static final CyclicBarrier barrier = new CyclicBarrier(CONCURRENCY); public static void main(String[] args) { for (int i = 0; i < CONCURRENCY; i++) { new Thread(() -> { try { for (int round = 1; round <= ROUNDS; round++) { System.out.println(Thread.currentThread().getName() + " 准备第 " + round + " 轮"); barrier.await(); // 等所有线程准备完毕 doRequest(); // 同时发起请求 barrier.await(); // 等所有线程完成本轮 } } catch (Exception e) { e.printStackTrace(); } }).start(); } } private static void doRequest() { try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }

4.6 屏障破坏与异常处理

CyclicBarrier有两种情况会破坏屏障:

  • 等待期间线程被中断:某个线程在await()时被打断,会调用breakBarrier()标记破坏,并唤醒其他等待线程,其他线程收到BrokenBarrierException。

  • 超时:带超时的await(timeout, unit)超时后,也会破坏屏障。

java

try { barrier.await(1, TimeUnit.SECONDS); } catch (TimeoutException e) { System.out.println("等待超时,屏障被破坏"); } catch (BrokenBarrierException e) { System.out.println("屏障已经被其他线程破坏"); }

需要注意的是,一旦屏障被破坏,所有等待线程都会收到BrokenBarrierException。若想继续使用,必须调用reset()重置屏障。但reset()会让当前正在等待的线程收到BrokenBarrierException,因此必须谨慎使用。

5. CountDownLatch 与 CyclicBarrier 的对比

经过前面的分析,我们可以把两者的差异总结成下面这张表:

对比维度CountDownLatchCyclicBarrier
核心语义一个或多个线程等待其他线程完成一组线程互相等待,齐了再一起走
复用能力一次性,不能重置可循环复用
底层实现AQS 共享模式ReentrantLock + Condition
计数方向递减到 0递减到 0 后重置为 parties
等待方角色通常是主线程等待子线程参与线程互相等待
触发动作无可以指定 barrierAction
异常处理中断抛 InterruptedException中断、超时都会破坏屏障
典型场景主线程等子任务、并发发令、启动就绪、超时保护分阶段并行计算、多轮压测、分片合并

用一句话总结:CountDownLatch 是「等别人干完」,CyclicBarrier 是「大家一起走」。前者是单向等待,后者是互相等待;前者一次性,后者可循环。

6. 常见误区与最佳实践

6.1 常见误区

误区一:把 CountDownLatch 当 CyclicBarrier 用。有些场景需要多个线程反复在多个阶段同步,却用了CountDownLatch,导致需要为每个阶段创建一个新的 latch,代码非常啰嗦。这类场景应该用CyclicBarrier。

误区二:忘记在 finally 中 countDown。如果任务执行中抛异常,没有在finally中调用countDown(),主线程会永远卡在await()上。正确的写法是把countDown()放在finally中。

误区三:CyclicBarrier 的 barrierAction 抛异常。如果barrierAction执行时抛出异常,CyclicBarrier会破坏屏障,所有等待线程都会收到BrokenBarrierException。因此barrierAction应该尽量简单,不要抛出异常。

误区四:忽略超时设置。在生产环境中,任何await()都应该设置超时,避免线程无限期挂起。

误区五:reset() 使用不当。reset()会让当前正在等待的线程收到BrokenBarrierException,因此只能在没有等待线程时使用,或者做好异常处理。

6.2 最佳实践

  • 明确语义选工具:单向等待用CountDownLatch,多方互等用CyclicBarrier。

  • countDown 放 finally:保证计数器一定能递减,避免主线程永久阻塞。

  • await 加超时:避免任务卡死导致整个链路不可用。

  • 线程池替代 new Thread:避免线程频繁创建销毁。

  • barrierAction 要简单:只做轻量的汇总、日志或状态更新,不要执行复杂逻辑。

  • 不要复用 CountDownLatch:如果需要在多个阶段同步,直接用 CyclicBarrier。

  • 异常要处理:InterruptedException、BrokenBarrierException、TimeoutException 都要有明确的处理策略。

7. 总结

CountDownLatch和CyclicBarrier是 JUC 中最常用的两个同步工具,它们解决的都是「多线程相互等待」的问题,但侧重点不同。

回顾全文,可以把核心结论压缩成下面几句话:

  1. CountDownLatch 是单向等待、一次性:一个或多个线程等待其他线程完成一组操作,计数器归零后释放所有等待线程,无法重置。

  2. CyclicBarrier 是多方互等、可循环:一组线程互相等待,到达屏障点后一起继续,且可以循环复用。

  3. 底层实现不同:CountDownLatch 基于 AQS 共享模式,CyclicBarrier 基于 ReentrantLock + Condition。

  4. 典型场景不同:CountDownLatch 适合主线程等子任务、并发发令、服务就绪、超时保护;CyclicBarrier 适合分阶段并行计算、多轮压测、分片合并。

  5. 使用要点:countDown 放 finally,await 加超时,barrierAction 要简单,异常要处理。

  6. 选型口诀:等别人干完用 CountDownLatch,大家一起走用 CyclicBarrier。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询