CountDownLatch 与 CyclicBarrier:区别、源码与实战详解

📅 2026/8/20 15:34:24
CountDownLatch 与 CyclicBarrier:区别、源码与实战详解
1. 引言在 Java 并发编程中CountDownLatch和CyclicBarrier是两个用于线程间协调的经典工具类。它们都基于AbstractQueuedSynchronizerAQS实现但设计理念和使用场景截然不同。本文将深入剖析两者的核心区别、底层源码实现并结合实战案例展示其典型用法帮助开发者精准选择并高效运用。2. 核心区别概览特性CountDownLatchCyclicBarrier核心机制一次性计数器减到 0 时释放所有等待线程可重复使用的屏障所有线程到达屏障后一起释放计数方向递减countDown递增await重置能力不可重置计数为 0 后失效可重置reset或自动重置换代主从角色主线程等待await多个子线程完成任务countDown多个线程相互等待地位对等典型场景启动前等待资源初始化、等待多个服务启动完成并行计算分阶段同步、多线程数据合并3. CountDownLatch 详解3.1 核心用法CountDownLatch通过一个计数器工作构造时指定初始值count。线程调用countDown()使计数器减 1调用await()的线程会阻塞直到计数器变为 0。// 典型用法主线程等待多个子线程完成初始化publicclassCountDownLatchDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{intworkerCount3;CountDownLatchlatchnewCountDownLatch(workerCount);for(inti0;iworkerCount;i){newThread(()-{try{// 模拟初始化工作Thread.sleep((long)(Math.random()*1000));System.out.println(Thread.currentThread().getName() 初始化完成);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{latch.countDown();// 任务完成计数器减1}},Worker-i).start();}System.out.println(主线程等待所有 Worker 初始化...);latch.await();// 阻塞直到计数器为0System.out.println(所有 Worker 初始化完成主线程继续执行);}}3.2 源码解读基于 OpenJDK 17CountDownLatch内部使用一个继承自AbstractQueuedSynchronizer的静态内部类Sync来实现同步。// java.util.concurrent.CountDownLatch.SyncprivatestaticfinalclassSyncextendsAbstractQueuedSynchronizer{Sync(intcount){setState(count);// 使用 AQS 的 state 存储计数器}intgetCount(){returngetState();}// 尝试获取共享锁只有当 state 0 时才成功即计数器为0protectedinttryAcquireShared(intacquires){return(getState()0)?1:-1;}// 尝试释放共享锁即执行 countDown将 state 减1protectedbooleantryReleaseShared(intreleases){// 自旋 CAS 减1for(;;){intcgetState();if(c0)returnfalse;// 已经为0无法再减intnextcc-1;if(compareAndSetState(c,nextc))returnnextc0;// 返回 true 表示本次减1后计数器变为0}}}关键点状态state即计数器构造时setState(count)。await() 原理调用await()会触发acquireSharedInterruptibly(1)最终调用tryAcquireShared。只要state ! 0就返回 -1导致线程进入 AQS 队列等待。countDown() 原理调用countDown()会触发releaseShared(1)最终调用tryReleaseShared。通过 CAS 将 state 减 1如果减后变为 0则返回 true这会唤醒所有在await()上等待的线程。3.3 实战场景服务启动等待等待所有微服务健康检查通过后再启动网关。并行任务汇总MapReduce 模型中等待所有 Map 任务完成后再启动 Reduce 任务。测试并发模拟高并发场景让所有线程同时开始执行。4. CyclicBarrier 详解4.1 核心用法CyclicBarrier允许一组线程相互等待直到所有线程都到达某个公共屏障点barrier后再一起继续执行。构造时可指定参与线程数parties以及可选的屏障动作barrierAction。// 典型用法多线程分阶段计算等待所有线程完成当前阶段publicclassCyclicBarrierDemo{publicstaticvoidmain(String[]args){intthreadCount3;CyclicBarrierbarriernewCyclicBarrier(threadCount,()-{System.out.println(所有线程已到达屏障开始下一阶段);});for(inti0;ithreadCount;i){newThread(()-{try{System.out.println(Thread.currentThread().getName() 开始第一阶段);Thread.sleep((long)(Math.random()*1000));barrier.await();// 等待其他线程System.out.println(Thread.currentThread().getName() 开始第二阶段);Thread.sleep((long)(Math.random()*1000));barrier.await();// 再次等待System.out.println(Thread.currentThread().getName() 完成所有阶段);}catch(InterruptedException|BrokenBarrierExceptione){Thread.currentThread().interrupt();}},Thread-i).start();}}}4.2 源码解读基于 OpenJDK 17CyclicBarrier内部使用ReentrantLock和Condition实现同步核心状态包括parties屏障的参与线程数count当前尚未到达屏障的线程数递减generation代表当前“代”每次屏障被打破或重置时创建新 generation// java.util.concurrent.CyclicBarrier 核心方法 await()publicintawait()throwsInterruptedException,BrokenBarrierException{try{returndowait(false,0L);}catch(TimeoutExceptiontoe){thrownewError(toe);// cannot happen}}privateintdowait(booleantimed,longnanos)throwsInterruptedException,BrokenBarrierException,TimeoutException{finalReentrantLocklockthis.lock;lock.lock();try{finalGenerationggeneration;if(g.broken)thrownewBrokenBarrierException();if(Thread.interrupted()){breakBarrier();thrownewInterruptedException();}intindex--count;// 当前线程到达计数器减1if(index0){// 最后一个线程到达booleanranActionfalse;try{finalRunnablecommandbarrierCommand;if(command!null)command.run();// 执行屏障动作ranActiontrue;nextGeneration();// 重置屏障唤醒所有等待线程return0;}finally{if(!ranAction)breakBarrier();}}// 不是最后一个线程进入等待for(;;){try{if(!timed)trip.await();// 在 Condition 上等待elseif(nanos0L)nanostrip.awaitNanos(nanos);}catch(InterruptedExceptionie){if(ggeneration!g.broken){breakBarrier();throwie;}else{Thread.currentThread().interrupt();}}if(g.broken)thrownewBrokenBarrierException();if(g!generation)// 屏障已换代返回到达序号returnindex;}}finally{lock.unlock();}}// 创建新一代屏障唤醒所有等待线程privatevoidnextGeneration(){trip.signalAll();// 唤醒所有在 Condition 上等待的线程countparties;// 重置计数器generationnewGeneration();// 创建新代}关键点可重用性当所有线程到达屏障后nextGeneration()会重置count parties并创建新的generation屏障可重复使用。屏障动作最后一个到达的线程执行barrierCommand如果存在然后才唤醒其他线程。中断处理线程在等待期间被中断会调用breakBarrier()将当前 generation 标记为 broken并唤醒所有等待线程。超时机制await(long timeout, TimeUnit unit)支持超时超时后屏障被打破。4.3 实战场景并行计算分阶段同步如多线程排序算法每完成一个阶段后同步数据。多线程数据合并多个线程分别处理数据的一部分全部完成后合并结果。游戏服务器同步多个玩家准备就绪后同时开始游戏。批量任务处理将大任务拆分为多个子任务所有子任务完成后执行汇总操作。5. 对比总结与选型建议5.1 核心差异对比维度CountDownLatchCyclicBarrier重用性一次性计数为0后失效可重复使用自动或手动重置计数方向递减countDown递减内部count但逻辑上是递增到达线程角色主从模式1个主线程等待N个子线程对等模式N个线程相互等待屏障动作无支持最后一个线程到达后执行异常处理计数不会重置中断或超时会导致屏障被打破BrokenBarrierException适用场景一等多、任务完成后触发多等多、分阶段同步5.2 选型指南选择 CountDownLatch 当需要一次性事件通知机制主线程需要等待多个子线程完成初始化或准备工作不需要重复使用同步点例如服务启动等待、测试并发开始信号选择 CyclicBarrier 当需要多个线程在某个点同步后继续执行需要重复使用同步屏障需要在所有线程到达后执行特定操作屏障动作例如并行计算分阶段、多轮游戏同步、批量数据处理5.3 混合使用示例在实际项目中两者可以结合使用// 使用 CountDownLatch 等待所有线程初始化完成// 使用 CyclicBarrier 进行多轮计算同步publicclassHybridDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{intthreadCount4;intphases3;CountDownLatchinitLatchnewCountDownLatch(threadCount);CyclicBarrierphaseBarriernewCyclicBarrier(threadCount);for(inti0;ithreadCount;i){newThread(()-{// 初始化阶段System.out.println(Thread.currentThread().getName() 初始化完成);initLatch.countDown();try{initLatch.await();// 等待所有线程初始化完成// 多阶段计算for(intphase1;phasephases;phase){System.out.println(Thread.currentThread().getName() 开始第 phase 阶段);Thread.sleep((long)(Math.random()*500));phaseBarrier.await();// 等待其他线程完成当前阶段}System.out.println(Thread.currentThread().getName() 所有阶段完成);}catch(Exceptione){Thread.currentThread().interrupt();}},Worker-i).start();}}}6. 常见问题与注意事项6.1 CountDownLatch 常见问题计数溢出countDown()调用次数超过初始计数不会报错但可能导致逻辑错误。不可重置计数为0后无法重用需要创建新实例。线程安全countDown()和await()本身是线程安全的但业务逻辑需要自行保证。超时等待使用await(long timeout, TimeUnit unit)避免永久阻塞。6.2 CyclicBarrier 常见问题屏障被打破线程中断、超时或屏障动作异常会导致屏障被打破所有等待线程收到BrokenBarrierException。重置风险调用reset()会打破当前屏障可能导致等待线程收到异常。屏障动作异常屏障动作抛出异常会导致屏障被打破。线程数匹配构造时指定的parties必须与实际调用await()的线程数一致。6.3 性能考量CountDownLatch基于 AQS适合一次性同步场景性能开销较小。CyclicBarrier基于ReentrantLock和Condition适合可重复使用的同步场景但每次屏障换代都有一定开销。在高并发场景下考虑使用PhaserJava 7作为更灵活的替代方案。7. 总结CountDownLatch和CyclicBarrier都是 Java 并发包中强大的线程协调工具但设计理念不同CountDownLatch是一次性的倒数门闩适合一等多的场景如主线程等待多个子线程完成任务。CyclicBarrier是可循环使用的屏障适合多等多的场景如多个线程需要分阶段同步。选择时需根据业务场景的同步需求、重用性要求和线程角色来决定。理解它们的源码实现有助于避免常见的并发陷阱编写出更健壮、高效的并发程序。在实际开发中还可以结合CompletableFuture、Phaser等更现代的并发工具构建更复杂的同步模式。