Java 中 Phaser/CyclicBarrier/CountDownLatch 区别?
三者可从计数方向、能否复用和参与者能否动态变化区分:CountDownLatch 一次性递减至归零,CyclicBarrier 可循环等待到齐,Phaser 支持多阶段且参与者可增删,应按实际场景选择。
三者的核心差别就三句话:CountDownLatch 是「一次性、计数递减、等归零」;CyclicBarrier 是「可循环、计数递增、等到齐」;Phaser 是「多阶段、参与者可动态增删」。CountDownLatch 和 CyclicBarrier 都自 JDK 1.5 引入,Phaser 自 JDK 7 引入,是前两者的「合体升级版」,但简单场景用它反而更啰嗦。
CountDownLatch、CyclicBarrier、Phaser 最核心的区别是什么?
结论:判断用哪个,只看三个维度——计数方向、能否复用、参与者数量能否动态变化。
| 维度 | CountDownLatch | CyclicBarrier | Phaser |
|---|---|---|---|
| 计数方向 | 构造时给 count,countDown() 递减到 0 | 构造时给 parties,await() 递增到 parties | 注册 parties,arrive() 递增到 parties |
| 能否复用 | 不能,归零即废,只能新建 | 能,自动进入下一代(generation) | 能,阶段(phase)自动递增 |
| 参与者动态调整 | 不能 | 不能 | 能,register() / arriveAndDeregister() |
| 线程角色 | 等待方与计数方可以不同(主从) | 所有线程对等,互相等待 | 二者皆可 |
| 底层实现 | AQS 共享模式 | ReentrantLock + Condition | 单个 long 型 state 位运算(高 32 位存 phase,低 32 位存 parties) |
一句话记忆:CountDownLatch 是「等别人做完」,CyclicBarrier 是「等大家到齐」,Phaser 是「等大家到齐,而且队伍可以换人、可以跑很多轮」。
CountDownLatch 怎么用?为什么它不能复用?
结论:CountDownLatch 适合「一个(或几个)线程等 N 个任务全部完成」的场景,计数一旦归零就永久失效,无法重置。
典型写法是主线程 await(),工作线程在 finally 里 countDown():
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try { /* 业务 */ } finally { latch.countDown(); }
}).start();
}
latch.await(); // 阻塞到计数为 0
// 或 latch.await(5, TimeUnit.SECONDS); 超时返回 false
关键点有两个:一是 countDown() 必须放 finally,否则任务抛异常会导致计数永远到不了 0、await() 永久阻塞;二是 getCount() 只用于监控和调试,不要拿它做业务判断。因为内部 AQS 的 state 只减不增,所以不存在 reset(),这也是它「一次性」的根本原因。顺带一提,如果是「等一组任务结果」而非单纯等完成,JDK 8 的 CompletableFuture.allOf() 通常是更好的选择。
CyclicBarrier 怎么用?和 CountDownLatch 有什么区别?
结论:CyclicBarrier 适合「固定 N 个线程反复汇合、每一轮都到齐才继续」的场景,比如多线程分片计算后统一进入下一轮迭代。
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("一轮结束"));
Runnable task = () -> {
for (int round = 0; round < 5; round++) {
// 本轮计算
barrier.await(); // 三个线程都到这里才一起放行
}
};
和 CountDownLatch 最本质的区别是「对等性」:CountDownLatch 里调 countDown() 的线程和调 await() 的线程可以是两拨人,职责不对称;CyclicBarrier 里每个线程都要 await(),谁也不能少,人也必须固定。第二个区别是可循环:一轮结束后自动开启下一代,构造参数里的 Runnable 会在每轮汇合点由最后一个到达的线程执行一次。
坑点:某个线程在 await() 时被中断或超时,屏障会「破损」,其余等待线程收到 BrokenBarrierException;手动调 reset() 同理。所以 await() 必须捕获这两个异常,并考虑是否重建屏障。用 isBroken() 可以查询破损状态。
Phaser 怎么用?什么场景必须用它?
结论:当「阶段数在运行时才能确定」或「参与线程会中途加入/退出」时,Phaser 是唯一选择,CountDownLatch 和 CyclicBarrier 都做不到。
Phaser phaser = new Phaser(1); // 主线程自己先注册 1 个
for (int i = 0; i < 3; i++) {
phaser.register(); // 运行时动态增加参与者
new Thread(() -> {
phaser.arriveAndAwaitAdvance(); // 阶段 0 汇合
phaser.arriveAndDeregister(); // 干完活退出,不再参与后续阶段
}).start();
}
phaser.arriveAndDeregister(); // 主线程注销,剩下的人继续
常用 API 分工明确:arrive() 只到达不等待,arriveAndAwaitAdvance() 到达并等本阶段所有人到齐,arriveAndDeregister() 到达后注销自己。查询用 getPhase()、getRegisteredParties()、getArrivedParties()、getUnarrivedParties()。想控制终止条件就重写 onAdvance(int phase, int registeredParties),返回 true 即终止 Phaser;forceTermination() 可强制结束。
两个注意点:一是 awaitAdvance(int phase) 不响应中断,需要可中断版本请用 awaitAdvanceInterruptibly();二是参数传错阶段号会立即返回,不会阻塞,调试时容易误判。此外 Phaser 支持构造分层结构(new Phaser(parent, parties)),用于海量线程分组汇合,这是 CyclicBarrier 完全没有的能力。
三者该怎么选?给一条判断标准
结论:一个线程等 N 个任务结束用 CountDownLatch;N 个线程反复互等到齐用 CyclicBarrier;阶段数不定或人数会变用 Phaser。
补充两条实践建议。第一,能用高层的 CompletableFuture、ExecutorService.invokeAll() 解决的任务编排,就别手写屏障,可读性更好;第二,所有等待都要带超时(await(timeout, unit)、awaitAdvanceInterruptibly(phase, timeout, unit)),否则一个卡住的任务会让整个线程池挂死。
收束一下:CountDownLatch 是单向的一次性倒计数,CyclicBarrier 是可循环的对等汇合点,Phaser 是把两者能力合并、再加动态注册和分层的重武器。选型先问自己三个问题——谁等谁、跑几轮、人数变不变,答案自然就出来了。
转载请注明出处,版权归原作者所有。