Java CyclicBarrier 与 CountDownLatch 对比
1. 引言
在本教程中,我们将比较 CyclicBarrier 和 CountDownLatch,并尝试理解它们之间的相似之处与差异。
2. 它们是什么?
在并发编程中,要理解每个工具的设计目的可能颇具挑战性。
首先,CountDownLatch 和 CyclicBarrier 都用于管理多线程应用程序。
此外,它们都旨在表达某个线程或一组线程应如何等待。
2.1 CountDownLatch
CountDownLatch 是一种同步机制:一个(或多个)线程会等待,而其他线程对这个“门闩”进行倒计数,直到计数器归零。
我们可以将其类比为餐厅里准备的一道菜。无论哪位厨师准备了多少个菜品中的某一部分,服务员都必须等到所有部分都摆上盘子后才能上菜。如果一道菜需要 n 个组成部分,那么任何一位厨师每放上一个部分,就会对这个门闩执行一次倒计数。
2.2 CyclicBarrier
CyclicBarrier 是一种可重用的同步机制:一组线程彼此相互等待,直到所有线程都到达屏障点。此时,屏障被打破,并可选择性地执行一个指定动作。
我们可以将其想象成一群朋友。每次他们计划一起去餐厅吃饭时,都会约定一个集合地点。大家在该地点互相等待,只有当所有人都到齐后,才能一起前往餐厅用餐。
3. 任务 vs 线程
让我们更深入地探讨这两个类在语义上的区别。
如前所述,CyclicBarrier 允许多个线程彼此相互等待,而 CountDownLatch 允许一个或多个线程等待若干任务完成。
简而言之,CyclicBarrier 维护的是线程数量,而 CountDownLatch 维护的是任务数量。
在以下代码中,我们定义了一个初始计数为 2 的 CountDownLatch,然后从同一个线程中调用两次 countDown():
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t = new Thread(() -> {
countDownLatch.countDown();
countDownLatch.countDown();
});
t.start();
countDownLatch.await();
assertEquals(0, countDownLatch.getCount());
一旦计数器归零,await() 调用就会返回。
注意,在此例中,同一个线程可以对计数器减两次。
然而,CyclicBarrier 在这一点上有所不同。
类似上面的例子,我们创建一个初始参与线程数为 2 的 CyclicBarrier,并从同一个线程中调用两次 await():
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
Thread t = new Thread(() -> {
try {
cyclicBarrier.await();
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// 错误处理
}
});
t.start();
assertEquals(1, cyclicBarrier.getNumberWaiting());
assertFalse(cyclicBarrier.isBroken());
这里第一个区别是:等待的线程本身就是屏障的一部分。
第二个、也是更重要的区别是:第二次调用 await() 实际上是无效的。单个线程无法使屏障“倒计数”两次。
事实上,由于线程 t 必须等待另一个线程也调用 await()(以使总参与线程数达到 2),因此 t 的第二次 await() 调用实际上不会被执行,除非屏障已经被打破!
在我们的测试中,屏障并未被打破,因为我们只有一个线程在等待,而不是所需的两个线程。这一点也可以通过 cyclicBarrier.isBroken() 方法返回 false 得到验证。
4. 可重用性
这两个类之间第二个最明显的区别是可重用性。
具体来说,当 CyclicBarrier 的屏障被触发(即所有线程都到达)后,其计数会重置为初始值;而 CountDownLatch 的计数一旦归零,就永远不会重置。
在以下代码中,我们定义了一个初始计数为 7 的 CountDownLatch,并通过 20 个不同的线程对其进行倒计数:
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,并让 20 个线程分别在其上调用 await():
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) {
// 错误处理
}
});
}
es.shutdown();
assertTrue(outputScraper.size() > 7);
在此情况下,我们观察到每当有新线程执行时,等待数量会变化,并且一旦屏障被触发(计数归零),它会自动重置为原始值,从而允许多次使用。
5. 结论
总而言之,CyclicBarrier 和 CountDownLatch 都是多线程同步中非常有用的工具。然而,它们在功能本质上存在显著差异。在选择使用哪一个时,务必根据具体场景仔细权衡。