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在内部封装了这些复杂性并借助AbstractQueuedSynchronizerAQS和ReentrantLock等底层机制提供了简洁、安全、高性能的等待语义。接下来我们分别深入它们。3. CountDownLatch 深度解析3.1 什么是 CountDownLatchCountDownLatch中文常译为“倒计时门闩”或“计数门闩”。它的核心语义可以用一句话概括一个或多个线程阻塞等待直到其他线程执行的一组操作全部完成计数器递减到 0所有等待线程被释放。你可以把它想象成一场赛跑比赛的发令流程先有 5 名裁判各自就位每有一位裁判就位计分板上的数字就减 1。运动员等待线程一直在起跑线上等待当计分板归零所有运动员同时获得出发资格。这里的“计分板”就是CountDownLatch内部维护的计数器。3.2 核心 APICountDownLatch的公开 API 非常精简一共只有三个常用方法方法说明CountDownLatch(int count)构造方法初始化计数器。count 必须大于等于 0void await()使当前线程阻塞等待直到计数器归零或线程被中断boolean await(long timeout, TimeUnit unit)带超时的等待超时返回 false计数器归零返回 truevoid countDown()将计数器减 1当计数器减到 0 时释放所有等待线程long getCount()返回当前计数器的值多用于调试或日志先看一个最简单的示例建立直观印象javaimport 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继承自 AQSAbstractQueuedSynchronizer。AQS 是 JUC 包的核心框架它维护了一个state字段和一个 FIFO 等待队列并支持两种资源共享模式独占模式Exclusive同一时刻只允许一个线程持有锁如ReentrantLock共享模式Shared同一时刻允许多个线程同时获取如Semaphore、CountDownLatch。CountDownLatch使用了共享模式。它把构造时传入的计数 count 保存在 AQS 的state中并重写了两个关键方法javaprotected 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不为 0tryAcquireShared就返回负数该线程被包装成节点加入 AQS 等待队列并阻塞调用countDown()的线程本质是释放共享资源。每次用 CAS 把state减 1当某次减完恰好变成 0 时tryReleaseShared返回 trueAQS 会唤醒等待队列中的所有线程这些线程再依次去重新尝试获取资源此时state 0全部获取成功。这里有两个非常值得注意的细节第一计数器不能重置。AQS 的state一旦归零就永远不会再变回正数。因为tryReleaseShared里明确判断了c 0直接返回 falseCountDownLatch也没有提供任何把 state 重新设大的方法。这就是它“一次性”的根本原因。第二唤醒是“全体唤醒”。当 state 减到 0 时AQS 会在共享模式下按传播机制唤醒队列中所有等待节点而不是只唤醒一个。这与“多个线程同时 await”的场景完美契合。3.4 中断与超时的处理await()方法是可以响应中断的。如果等待线程在阻塞期间被调用了interrupt()它会抛出InterruptedException并退出等待。因此在实际编码中要么显式捕获处理要么让异常向上抛出并注意不要吞掉中断状态。如果希望“最多等一段时间”可以使用带超时的版本javaCountDownLatch 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 次网络耗时而并行调用则只需要等待最慢的那一次。javaimport 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); ConcurrentLinkedQueueString 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()大家同时起跑。javaimport 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); // 全部完成信号 SetLong 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()全部就绪后一次性放行。javaimport 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()超时后执行降级逻辑。javaimport 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 什么是 CyclicBarrierCyclicBarrier中文常译为「循环屏障」或「栅栏」。它的语义同样可以用一句话概括让一组线程互相等待直到所有线程都到达同一个屏障点后再一起继续向后执行该屏障可以循环使用。如果说CountDownLatch像「发令枪」或者「终点线」那么CyclicBarrier更像「团建集合点」所有人到齐拍个照然后一起进入下一个环节。每次人都到齐集合点又能继续用于下一轮集合因此它能循环使用。4.2 核心 APICyclicBarrier的 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()将屏障重置到初始状态先看一个简单示例javaimport 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 与 ConditionCyclicBarrier并没有像CountDownLatch那样直接基于 AQS 的共享模式而是基于ReentrantLock和Condition自己实现等待队列。其核心字段大致如下lockReentrantLock用于保护屏障的内部状态tripCondition用于阻塞和唤醒等待线程parties每轮需要到达的线程数barrierCommand所有线程到齐后要执行的动作generation当前「代」对象用于标记屏障是否被破坏count当前这一轮尚未到达的线程数量。简化后的await()流程如下javapublic 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(); } }其中有两个关键方法javaprivate 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可以复用靠的是两点计数器可重置每一轮结束时会执行count parties把倒计时重新拉满代际更替每一轮使用不同的Generation对象标记状态既能区分不同轮次又能避免旧一轮的线程误入新一轮。这里的Generation就像比赛中的「场次号」。第一场到齐后屏障进入第二场后续到达的线程属于第二场与第一场互不干扰。这也是为什么CyclicBarrier适合「分阶段并行计算」类场景。4.5 典型使用场景4.5.1 场景一分阶段并行计算假设需要 3 个线程分别计算一部分数据计算分 3 个阶段进行且每个阶段都必须等所有线程完成上一阶段后才能开始。这种场景用CyclicBarrier表达非常自然。javaimport 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 场景二多线程数据加载后统一处理在数据量较大的场景中经常需要把一份大任务拆成若干份并行处理。例如从多个分片读取数据、做初步清洗然后等所有分片都处理完再统一做合并。javaimport 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可以很方便地实现。javaimport 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)超时后也会破坏屏障。javatry { 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 中最常用的两个同步工具它们解决的都是「多线程相互等待」的问题但侧重点不同。回顾全文可以把核心结论压缩成下面几句话CountDownLatch 是单向等待、一次性一个或多个线程等待其他线程完成一组操作计数器归零后释放所有等待线程无法重置。CyclicBarrier 是多方互等、可循环一组线程互相等待到达屏障点后一起继续且可以循环复用。底层实现不同CountDownLatch 基于 AQS 共享模式CyclicBarrier 基于 ReentrantLock Condition。典型场景不同CountDownLatch 适合主线程等子任务、并发发令、服务就绪、超时保护CyclicBarrier 适合分阶段并行计算、多轮压测、分片合并。使用要点countDown 放 finallyawait 加超时barrierAction 要简单异常要处理。选型口诀等别人干完用 CountDownLatch大家一起走用 CyclicBarrier。