Java CyclicBarrier 与 CountDownLatch 对比

更新于 2025-12-29

Java CyclicBarrier 与 CountDownLatch 对比

1. 引言

在本教程中,我们将比较 CyclicBarrierCountDownLatch,并尝试理解它们之间的相似之处与差异。

2. 它们是什么?

在并发编程中,要理解每个工具的设计目的可能颇具挑战性。

首先,CountDownLatchCyclicBarrier 都用于管理多线程应用程序。

此外,它们都旨在表达某个线程或一组线程应如何等待。

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. 结论

总而言之,CyclicBarrierCountDownLatch 都是多线程同步中非常有用的工具。然而,它们在功能本质上存在显著差异。在选择使用哪一个时,务必根据具体场景仔细权衡。