资讯动态

Java CountDownLatch vs CyclicBarrier:多线程协同的区别、复用与超时控制

发布时间:2026/8/11 20:36:42 来源:尧图企业网站定制
Java CountDownLatch vs CyclicBarrier:多线程协同的区别、复用与超时控制主线程要「等 5 个初始化任务全跑完再启动服务」,你随手用了个Thread.join(),结果任务是丢进线程池的、根本没有 Thread 对象可 join。又或者你想让一批 worker「都准备好了再同时开跑」做压测,发现越等越乱。这两类「多线程等齐了再走」的需求,JUC 里有两把趁手的锁——CountDownLatch和CyclicBarrier。它们长得像,用错场景就别扭。这篇讲清楚各自适合什么、怎么加超时,以及那个「只能用一次」的关键区别。场景一:主线程等一批任务全部完成 → CountDownLatchCountDownLatch就是个倒数计数器:初始化一个数字,每个任务干完调countDown()减一,await()的线程一直阻塞,直到计数归零。importjava.util.concurrent.*;publicclassStartupGate{publicstaticvoidmain(String[]args)throwsInterruptedException{inttaskCount5;CountDownLatchlatchnewCountDownLatch(taskCount);// 计数 5ExecutorServicepoolExecutors.newFixedThreadPool(5);for(inti0;itaskCount;i){finalintidi;pool.submit(()-{try{initModule(id);// 干活:初始化某个模块}finally{latch.countDown();// 必须放 finally,否则异常会漏减,主线程永久卡死}});}latch.await();// 主线程在这里等,直到 5 个都 countDownSystem.out.println(所有模块就绪,启动服务);pool.shutdown();}staticvoidinitModule(intid){try{Thread.sleep(100*id);}catch(InterruptedExceptionignored){}System.out.println(模块 id 初始化完成);}}两个要点:countDown()一定放finally。任务里抛异常没减计数,await()就永远等不到 0,主线程死等——这是最常见的线上卡死。这是一种「一等多」:一个(或多个)线程等多个任务完成。反过来「多等一」也能做:N 个 workerawait()一个计数为 1 的 latch,主线程countDown()一声令下,所有 worker 同时冲出——常用于并发压测的「发令枪」。场景二:一批线程互相等齐,再一起继续 → CyclicBarrierCyclicBarrier是「所有参与线程都到达栅栏点,才一起放行」。它不是主线程等任务,而是参与者之间互相等。importjava.util.concurrent.*;publicclassRaceStart{publicstaticvoidmain(String[]args){intplayers4;// 第二个参数:所有线程到齐后,由最后一个到达的线程执行的「汇合动作」CyclicBarrierbarriernewCyclicBarrier(players,()-System.out.println( 4 名选手就位,发令! ));for(inti0;iplayers;i){finalintidi;newThread(()-{try{System.out.println(选手 id 准备中);barrier.await();// 到栅栏,等其他人;到齐后一起返回System.out.println(选手 id 起跑);}catch(InterruptedException|BrokenBarrierExceptione){e.printStackTrace();}}).start();}}}输出会是 4 个「准备中」乱序,然后「发令」,再 4 个「起跑」。栅栏保证没到齐谁都别想过。那个可选的汇合动作(barrierAction)很实用:比如分阶段计算,每轮所有 worker 算完各自分片后,由汇合动作合并结果、再进入下一轮。核心区别:一次性 vs 可循环名字已经剧透了:Cyclic(可循环)。这是两者最本质的差别:CountDownLatch计数减到 0 就废了,不能重置。要再等一轮,得 new 一个新的。CyclicBarrier放行后自动重置,可以反复用于多轮同步。CyclicBarrierbarriernewCyclicBarrier(3);// 第一轮:3 个线程 await 到齐、放行// 栅栏自动重置// 第二轮:同样 3 个线程再 await,又能到齐放行 —— Latch 做不到这点所以「分阶段迭代计算」「多轮压测」这类重复同步只能用CyclicBarrier;「等一堆初始化完成一次」用CountDownLatch更轻。一张对照表帮你选:维度CountDownLatchCyclicBarrier谁等谁线程等事件/任务完成一组线程互相等到齐计数由谁减别的线程调countDown()线程自己调await()到达能否复用不能,归零即废能,放行后自动重置到齐回调无支持 barrierAction典型场景主线程等初始化 / 发令枪分阶段并行计算 / 多轮同步别忘了超时:两者都支持带时限的等待生产代码里,无限期await()是定时炸弹——一个任务卡住,等待方就永久阻塞。两者都提供带超时的重载:// CountDownLatch:超时返回 boolean,false 表示没等到就超时了if(!latch.await(3,TimeUnit.SECONDS)){System.out.println(有模块 3 秒内没就绪,走降级逻辑);}// CyclicBarrier:超时抛 TimeoutException,且会「打破」栅栏try{barrier.await(3,TimeUnit.SECONDS);}catch(TimeoutExceptione){// 栅栏被打破,其他还在 await 的线程会收到 BrokenBarrierExceptionSystem.out.println(没到齐,栅栏破了);}注意CyclicBarrier的连带效应:任何一个线程超时、被中断或抛异常,栅栏都会进入 broken 状态,其余正在await的线程会收到BrokenBarrierException。这是有意设计——协同失败就整体失败,别让一部分线程卡在破栅栏上。要恢复得调barrier.reset()。一个常见误用:用 Latch 硬凑循环同步见过有人为了多轮同步,每轮都 new 一个CountDownLatch:// 别扭:每轮重新创建,还要在多线程间安全地替换引用for(intround0;round3;round){CountDownLatchlatchnewCountDownLatch(workerCount);// ...每个 worker 干完 countDown,主线程 await...// 引用怎么传给 worker?volatile?每轮同步?一堆麻烦}这正是CyclicBarrier存在的意义:需要重复的、参与者之间的同步,直接上 CyclicBarrier,一个对象用到底,别拿 Latch 硬凑。反过来,只需一次性等待就别上 Barrier,Latch 更简单。小结CountDownLatch:倒数计数器,线程等一批任务完成,一次性、归零即废;countDown()务必放finally。CyclicBarrier:栅栏,一组线程互相等到齐再一起走,可循环复用,支持到齐回调。选型口诀:一次性等完成 → Latch;多轮/相互等齐 → Barrier。生产环境永远用带超时的await;CyclicBarrier任一线程失败会打破栅栏,其余线程抛BrokenBarrierException,需reset()恢复。一句话记忆:Latch 是「等你们干完我再走」的一次性关卡,Barrier 是「等咱们到齐一起走」的可复用栅栏。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价