CountDownLatch 与 CyclicBarrier 哪个可以用?为什么
CountDownLatch 与 CyclicBarrier 哪个可以用?为什么
面试指北 · 去哪儿
题意
这道题表面上是在问「CountDownLatch 和 CyclicBarrier 哪个可以用」,实际考察的是:你是否理解这两个同步工具类的语义差异,以及能不能根据场景做选择。
面试官通常不是要你二选一背答案,而是想听到类似这样的判断:
- 两者都能用,但解决的问题不一样;
CountDownLatch更像「等待事件完成」:一个或多个线程等另外一些线程把任务做完;CyclicBarrier更像「线程集合点」:一组线程互相等待,等大家都到齐了再一起继续;- 如果是一次性等待,选
CountDownLatch;如果是多阶段、可重复的线程同步,选CyclicBarrier。
结论
CountDownLatch 和 CyclicBarrier 都可以用于线程同步,但适用场景不同:
- 只需要等待一批任务完成一次,例如主线程等待多个工作线程处理完再汇总,用
CountDownLatch更自然; - 多个线程需要在某个阶段互相等待,并且这个同步点要重复使用,例如多阶段并行计算,每一阶段结束后所有线程会合再进入下一阶段,用
CyclicBarrier更自然。
一句话概括:
CountDownLatch等的是「事情做完」,CyclicBarrier等的是「大家都到齐」。
如果面试中只能给一个选择依据,可以回答:看是否需要重复使用、看等待关系是“等任务完成”还是“线程之间互相等待”。
要点展开
1. CountDownLatch 的特点
定义
CountDownLatch 是一个倒计数器。构造时指定一个计数 count,线程可以调用 countDown() 把计数减一,其他线程调用 await() 阻塞等待,直到计数减到 0 后被唤醒。
它的核心特点是:计数器只能减到 0,不能重置,所以通常是一次性使用的同步工具。
适用场景
- 等待多线程任务完成
主线程启动多个工作线程处理数据,所有工作线程完成后,主线程再汇总结果。
- 等待资源初始化完成
一个线程等待其他线程完成初始化操作后再继续执行。
- 也可以反过来做“启动信号”
多个工作线程先 await() 等待,主线程初始化完成后调用一次 countDown(),所有工作线程同时被放行。这种用法本质上还是「等一个事件发生」。
关键方法
countDown():计数器减 1,当计数器减到 0 时,所有等待线程被唤醒;await():调用线程阻塞,直到计数器减到 0;await(long timeout, TimeUnit unit):带超时等待,超时返回false,不会一直卡死。
代码示例
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " finished task.");
latch.countDown();
}).start();
}
try {
latch.await();
System.out.println("All tasks finished. Main thread continues.");
} catch (InterruptedException e) {
e.printStackTrace();
}
工程代码里更建议把 countDown() 放在 finally 中,避免任务抛异常导致计数器永远减不到 0。
限制
CountDownLatch是一次性工具,计数器无法重置;- 如果需要重复使用类似功能,需要重新创建一个新的
CountDownLatch实例; - 它不要求等待线程数等于计数器值。构造参数表示需要
countDown()的次数,一个线程可以调用多次countDown(),多个线程也可以等待同一个 latch。
2. CyclicBarrier 的特点
定义
CyclicBarrier 是一个屏障。一组线程各自执行到屏障点后调用 await(),直到所有线程都到达屏障,大家才一起继续执行。
它的特点是:可以重复使用。每一轮所有线程都到达屏障后,屏障会进入下一轮,线程可以继续在后续阶段再次 await()。
适用场景
- 让多线程在某个阶段完成同步
所有线程到达某个点后再一起继续执行,例如多线程分段计算结果,等所有线程都完成当前阶段后再汇总或进入下一阶段。
- 阶段性任务
线程在每个阶段执行完成后等待彼此同步,再继续下一阶段。因为 CyclicBarrier 可循环使用,所以很适合这种多轮同步。
关键方法
await():每个线程调用此方法表示到达屏障;当所有线程都调用await()后,所有线程继续执行;- 可以在构造时指定一个
Runnable,当所有线程到达屏障后,由最后一个到达屏障的线程执行这个额外任务; await(long timeout, TimeUnit unit):带超时等待,超时抛TimeoutException,并可能导致屏障进入 broken 状态。
代码示例
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads reached the barrier. Performing final task...");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " finished task.");
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
特点
- 支持重复使用,适合多个阶段的同步操作;
- 提供屏障任务
Runnable,便于处理阶段性的额外工作; - 屏障任务由最后一个到达屏障的线程执行,不要在屏障任务里再次
await()同一个屏障,否则容易死锁; - 如果某个线程在等待时被中断、超时,或者屏障任务抛异常,屏障可能被标记为 broken,其他等待线程会收到
BrokenBarrierException。
3. CountDownLatch 和 CyclicBarrier 的比较
| 特性 | CountDownLatch |
CyclicBarrier |
|---|---|---|
| 作用 | 等待其他线程完成任务或事件发生 | 等待所有线程到达屏障 |
| 重用性 | 不可重用 | 可重用 |
| 线程数关系 | 等待线程数和 countDown() 次数可以不同,一个线程可多次 countDown() |
参与线程数通常固定为构造时的 parties |
| 额外任务 | 不支持内置的“到齐后执行任务” | 支持在屏障点执行额外任务 |
| 典型场景 | 主线程等待工作线程完成;资源初始化;启动信号 | 多线程阶段性同步;多轮并行计算 |
| 阻塞语义 | 等计数归零 | 等所有参与线程到齐 |
4. 选择依据
选择 CountDownLatch
- 一次性任务:只需要等待线程完成一次操作,例如主线程等待工作线程处理完毕;
- 等待多个资源初始化完成:初始化线程完成后通知等待线程;
- 需要“一个线程等多个线程”或“多个线程等一个线程”:
CountDownLatch的等待关系更灵活; - 不需要重置计数器,也不需要所有等待线程都参与计数。
选择 CyclicBarrier
- 多阶段任务:需要让线程在某个点同步,并反复使用该屏障;
- 固定线程数的集合点:每次都需要固定数量的线程齐聚屏障才能继续;
- 需要在所有线程到齐后执行一个阶段性动作,可以用
CyclicBarrier的屏障任务。
易错点与对比
- CountDownLatch 不是“所有线程必须 await”
它只要求计数减到 0。可以是一个主线程 await(),多个工作线程 countDown();也可以是多个线程 await(),一个线程负责 countDown()。
- countDown() 建议放在 finally
如果任务执行中抛异常,没有执行 countDown(),计数器可能永远减不到 0,等待线程会一直阻塞。
- CountDownLatch 不能重置
计数到 0 后,后续再调用 await() 通常会立即返回;如果需要下一轮同步,必须新建一个 CountDownLatch。
- CyclicBarrier 的 parties 要和参与线程数匹配
如果构造时是 3,但只有 2 个线程调用 await(),屏障不会被触发,线程会一直等待,除非使用超时或发生异常。
- CyclicBarrier 的屏障任务由最后到达的线程执行
屏障任务不是额外开一个线程执行,而是最后一个到达屏障的线程执行。屏障任务应尽量短,且不要在里面再次等待同一个屏障。
- 异常会破坏屏障
CyclicBarrier.await() 可能抛出 InterruptedException、TimeoutException、BrokenBarrierException。某个线程异常后,其他线程可能被 broken 屏障唤醒并抛异常,不能简单吞掉。
- 两者都不是锁,也不解决共享资源竞争
CountDownLatch 和 CyclicBarrier 解决的是线程之间的等待与同步顺序,不负责保护共享变量。共享数据仍然需要锁、原子类或其他并发容器。
- 底层实现可以当作加分项
CountDownLatch 通常基于 AQS 的共享模式实现;CyclicBarrier 通常基于 ReentrantLock 和 Condition 实现。面试中先讲清语义和场景,再补底层实现更稳妥。
小结
- 如果任务是一次性的,主要是等任务完成、等事件发生,用
CountDownLatch; - 如果任务是多阶段的,需要一组线程互相等待并且可重复同步,用
CyclicBarrier; - 回答这道题时,不要只说“CountDownLatch 不能重用、CyclicBarrier 能重用”,还要讲清楚:
CountDownLatch等的是计数归零,CyclicBarrier等的是线程到齐。
面试表达可以收束为一句:两者都能用,但 CountDownLatch 适合一次性等待任务完成,CyclicBarrier 适合多线程多阶段会合;选哪个取决于同步语义和是否需要复用。