java并发系列-Java CyclicBarrier vs CountDownLatch

1 介绍

这篇文章,我们通过比较 CyclicBarrierCountDownLatch 来理解它们之间的相同点和不同点。

2 这些是什么?

当与并发有关时,怎样概念化它们试图要完成的事情是个挑战。

最重要的一点是,CyclicBarrierCountDownLatch 都用于管理多线程应用。并且,它们都试图表达一个给定线程或者线程组应该怎样等待。

2.1 CountDownLatch

CountDownLatch 结构,一个线程会在 latch 上等待,直到其他线程在 latch 上倒数并到达 0。

我们可以把这想象成餐厅正在准备的菜肴。不管哪个厨师来做,服务员都要等待餐盘装满 n 份菜肴为止。如果餐盘要装 n 菜肴,任何厨师在他放一个菜肴在餐盘时都要在 latch 上 count down

2.2 CyclicBarrier

CyclicBarrier 是可重用的结构,一组线程一起等待直到所有线程都到达。都到达后,barrier 就会被打破并执行指定动作。

我们可以把这想象成一组朋友。每次他们计划在餐馆聚餐,他们都要商量一个碰头的地方。他们在那里等待,直到所有人都到了才会一起去餐馆。

2.3 更进一步阅读

更多细节,请阅读之前的教程 CountDownLatchCyclicBarrier

3. Tasks vs. Threads

让我们深入挖掘下这两个类语义上的不同。

如定义所述,CyclicBarrier 支持指定数目的线程互相等待, 而 CountDownLatch 支持一个或者多个线程等待指定数目的任务完成。

简单地讲,CyclicBarrier 维护指定数据的线程而 CountDownLatch 维护指定数目的任务。

在下面代码中,我们定义一个 CountDownLatch 以及计数为 2 。然后,我们在一个线程中调用 countDown() 两次:

1
2
3
4
5
6
7
8
9
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t = new Thread(() -> {
countDownLatch.countDown();
countDownLatch.countDown();
});
t.start();
countDownLatch.await();

assertEquals(0, countDownLatch.getCount());

一旦 latch 到达 0,对 await() 的调用会返回。

注意这里,我们使同一个线程对 count 减少了两次。

CyclicBarrier,在这一点上,完全不同。

和上面例子类似,我们创建一个 CyclicBarrier*,再次传入 count 为 2,并在同一线程中调用 *await() 两次:

1
2
3
4
5
6
7
8
9
10
11
12
13
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());

这里的第一个区别是,等待的线程本身就是障碍(barrier)。

第二个区别是,第二个 await() 没有用。当个线程不能 count down 一个 barrier 两次。

事实上,因为 t 必须等待另外的线程调用 await() - 使 count 到达 2 - t 对 await() 的第二次调用实际上不会触发直到 barrier 被打破。

在我们的测试汇总,barrier 没有跨过去,因为我们只有一个线程等待而想要 barrier 被打破需要两个线程。这也可以通过 cyclicBarrier.isBroken() 方法证明,因为它返回 false

4. 可重用性

这两个类最显而易见的区别是可重用性。当 CyclicBarrier 中的 barrier 被打破,count 会重置为原始值。CountDownLatch 的 count 永远不会重置。

下面代码,我们定义一个 CountDownLatch 和 count 为 7,然后 20 个不同线程调用 count down:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(),count 到达 0 后也不会重置。

类似的,我们定义一个 CyclicBarrier 和 count 为 7,然后 20 个不同线程等待:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);

这里,我们观察到每次一个新线程运行,count就会减一,一旦它到达 0,就会重置为原始值。

5. 总结

总的来说, CountDownLatchCyclicBarrier 对于多线程同步都很有帮助。然而,他们提供的功能有根本性的不同。当使用它们处理任务时,要小心选择哪个更合适。


本文为译文,作者通过翻译达到学习目的。 原文链接 | 原文源码链接 | 本站源码链接