1. 简介
在本篇文章中,我们将对 CyclicBarrier
和 CountDownLatch
进行对比分析,帮助大家理解它们的相似点和核心差异。
2. 它们是做什么的?
在并发编程中,这两个工具类常常让人傻傻分不清楚。但其实只要抓住它们的核心用途,就能轻松区分。
✅ 共同点:
- 都是用于管理多线程程序的同步工具
- 都可以让一个或多个线程进入等待状态,直到某个条件满足
2.1. CountDownLatch
CountDownLatch
是一种计数器结构,主线程调用 await()
方法进行等待,其他线程则通过调用 countDown()
来减少计数器值,当计数器归零时,所有等待线程被唤醒继续执行。
可以把它想象成餐厅里的一道菜。不管是由哪个厨师完成这道菜中的 n 个组成部分,服务员都必须等所有部分都上齐了才能端给客人。每有一个部分完成,就调用一次 countDown()
。
2.2. CyclicBarrier
CyclicBarrier
是一个可重用的屏障机制,它让一组线程互相等待,直到全部线程都调用了 await()
方法后,屏障才会打开,并且可以选择性地执行一个回调动作。
就像一群朋友约饭,他们约定好在一个地点集合。只有所有人都到齐了,才能一起去餐厅吃饭。
2.3. 深入学习建议
如果你还想深入了解这两个类的更多细节,可以参考我们之前的教程:
3. 任务 vs 线程模型
接下来我们从语义层面深入剖析一下两者的本质区别。
📌 关键点总结:
CountDownLatch
维护的是任务数量(task count)CyclicBarrier
维护的是线程数量(thread count)
来看下面这段代码:
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t = new Thread(() -> {
countDownLatch.countDown();
countDownLatch.countDown();
});
t.start();
countDownLatch.await();
assertEquals(0, countDownLatch.getCount());
这里我们创建了一个计数为 2 的 CountDownLatch
,然后由同一个线程连续调用了两次 countDown()
。最终计数归零,主线程从 await()
返回。
⚠️ 注意:同一个线程是可以多次调用 countDown()
的。
再来看 CyclicBarrier
:
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
Thread t = new Thread(() -> {
try {
cyclicBarrier.await();
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// error handling
}
});
t.start();
assertEquals(1, cyclicBarrier.getNumberWaiting());
assertFalse(cyclicBarrier.isBroken());
❌ 第二次 await()
实际上是无效的!因为 同一个线程不能两次“抵达”屏障。
在这个例子中,虽然线程调用了两次 await()
,但由于没有第二个线程参与,屏障永远不会触发,因此第一个 await()
就会阻塞住,导致第二个永远不会被执行。这也解释了为什么 isBroken()
返回 false
—— 屏障还没触发就被卡住了。
4. 可重用性对比
这是两者之间最明显的差异之一。
✅ CyclicBarrier 的特点:
- 当所有线程都到达屏障后,屏障会被重置,计数恢复初始值
- 所以它是可重用的
❌ CountDownLatch 的特点:
- 计数只能递减,不能重置
- 一旦归零就不能再用了
来看下面的例子:
CountDownLatch countDownLatch = new CountDownLatch(7);
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
es.execute(() -> {
long prevValue = countDownLatch.getCount();
countDownLatch.countDown();
if (countDownLatch.getCount() != prevValue) {
outputScraper.add("Count Updated");
}
});
}
es.shutdown();
assertTrue(outputScraper.size() <= 7);
尽管有 20 个线程参与 countDown()
,但由于计数器不会重置,所以最多只会有 7 次有效更新。
而使用 CyclicBarrier
的情况就不一样了:
CyclicBarrier cyclicBarrier = new CyclicBarrier(7);
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
es.execute(() -> {
try {
if (cyclicBarrier.getNumberWaiting() <= 0) {
outputScraper.add("Count Updated");
}
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// error handling
}
});
}
es.shutdown();
assertTrue(outputScraper.size() > 7);
由于屏障是可重用的,每当有 7 个线程到达后,屏障就会重置,接着下一批线程又可以再次触发,因此输出结果会超过 7 条记录。
5. 总结
总的来说,CyclicBarrier
和 CountDownLatch
都是非常实用的并发控制工具,但在功能设计上有本质区别:
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
计数对象 | 任务数量 | 线程数量 |
是否可重用 | ❌ 不可重用 | ✅ 可重用 |
使用场景 | 主线程等待多个子任务完成 | 多线程协作同步 |
📌 选型建议:
- 如果你需要一个主线程等待若干任务完成,选择
CountDownLatch
- 如果你需要多个线程彼此等待后再一起执行下一步,选择
CyclicBarrier
一如既往,文中涉及的所有示例代码都可以在 GitHub 上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-concurrency-advanced-2