1. 简介

在本篇文章中,我们将对 CyclicBarrierCountDownLatch 进行对比分析,帮助大家理解它们的相似点和核心差异。

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

总的来说,CyclicBarrierCountDownLatch 都是非常实用的并发控制工具,但在功能设计上有本质区别:

特性 CountDownLatch CyclicBarrier
计数对象 任务数量 线程数量
是否可重用 ❌ 不可重用 ✅ 可重用
使用场景 主线程等待多个子任务完成 多线程协作同步

📌 选型建议:

  • 如果你需要一个主线程等待若干任务完成,选择 CountDownLatch
  • 如果你需要多个线程彼此等待后再一起执行下一步,选择 CyclicBarrier

一如既往,文中涉及的所有示例代码都可以在 GitHub 上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-concurrency-advanced-2


原始标题:Java CyclicBarrier vs CountDownLatch