" />
文章

CountDownLatch 与 CyclicBarrier 哪个可以用?为什么

CountDownLatch 与 CyclicBarrier 哪个可以用?为什么

面试指北 · 去哪儿

题意

这道题表面上是在问「CountDownLatch 和 CyclicBarrier 哪个可以用」,实际考察的是:你是否理解这两个同步工具类的语义差异,以及能不能根据场景做选择。

面试官通常不是要你二选一背答案,而是想听到类似这样的判断:

  • 两者都能用,但解决的问题不一样;
  • CountDownLatch 更像「等待事件完成」:一个或多个线程等另外一些线程把任务做完;
  • CyclicBarrier 更像「线程集合点」:一组线程互相等待,等大家都到齐了再一起继续;
  • 如果是一次性等待,选 CountDownLatch;如果是多阶段、可重复的线程同步,选 CyclicBarrier。

结论

CountDownLatch 和 CyclicBarrier 都可以用于线程同步,但适用场景不同:

  • 只需要等待一批任务完成一次,例如主线程等待多个工作线程处理完再汇总,用 CountDownLatch 更自然;
  • 多个线程需要在某个阶段互相等待,并且这个同步点要重复使用,例如多阶段并行计算,每一阶段结束后所有线程会合再进入下一阶段,用 CyclicBarrier 更自然。

一句话概括:

CountDownLatch 等的是「事情做完」,CyclicBarrier 等的是「大家都到齐」。

如果面试中只能给一个选择依据,可以回答:看是否需要重复使用、看等待关系是“等任务完成”还是“线程之间互相等待”。

要点展开

1. CountDownLatch 的特点

定义

CountDownLatch 是一个倒计数器。构造时指定一个计数 count,线程可以调用 countDown() 把计数减一,其他线程调用 await() 阻塞等待,直到计数减到 0 后被唤醒。

它的核心特点是:计数器只能减到 0,不能重置,所以通常是一次性使用的同步工具。

适用场景

  1. 等待多线程任务完成

主线程启动多个工作线程处理数据,所有工作线程完成后,主线程再汇总结果。

  1. 等待资源初始化完成

一个线程等待其他线程完成初始化操作后再继续执行。

  1. 也可以反过来做“启动信号”

多个工作线程先 await() 等待,主线程初始化完成后调用一次 countDown(),所有工作线程同时被放行。这种用法本质上还是「等一个事件发生」。

关键方法

  • countDown():计数器减 1,当计数器减到 0 时,所有等待线程被唤醒;
  • await():调用线程阻塞,直到计数器减到 0;
  • await(long timeout, TimeUnit unit):带超时等待,超时返回 false,不会一直卡死。

代码示例

CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " finished task.");
        latch.countDown();
    }).start();
}

try {
    latch.await();
    System.out.println("All tasks finished. Main thread continues.");
} catch (InterruptedException e) {
    e.printStackTrace();
}

工程代码里更建议把 countDown() 放在 finally 中,避免任务抛异常导致计数器永远减不到 0。

限制

  • CountDownLatch 是一次性工具,计数器无法重置;
  • 如果需要重复使用类似功能,需要重新创建一个新的 CountDownLatch 实例;
  • 它不要求等待线程数等于计数器值。构造参数表示需要 countDown() 的次数,一个线程可以调用多次 countDown(),多个线程也可以等待同一个 latch。

2. CyclicBarrier 的特点

定义

CyclicBarrier 是一个屏障。一组线程各自执行到屏障点后调用 await(),直到所有线程都到达屏障,大家才一起继续执行。

它的特点是:可以重复使用。每一轮所有线程都到达屏障后,屏障会进入下一轮,线程可以继续在后续阶段再次 await()。

适用场景

  1. 让多线程在某个阶段完成同步

所有线程到达某个点后再一起继续执行,例如多线程分段计算结果,等所有线程都完成当前阶段后再汇总或进入下一阶段。

  1. 阶段性任务

线程在每个阶段执行完成后等待彼此同步,再继续下一阶段。因为 CyclicBarrier 可循环使用,所以很适合这种多轮同步。

关键方法

  • await():每个线程调用此方法表示到达屏障;当所有线程都调用 await() 后,所有线程继续执行;
  • 可以在构造时指定一个 Runnable,当所有线程到达屏障后,由最后一个到达屏障的线程执行这个额外任务;
  • await(long timeout, TimeUnit unit):带超时等待,超时抛 TimeoutException,并可能导致屏障进入 broken 状态。

代码示例

CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("All threads reached the barrier. Performing final task...");
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " finished task.");
        try {
            barrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();
}

特点

  • 支持重复使用,适合多个阶段的同步操作;
  • 提供屏障任务 Runnable,便于处理阶段性的额外工作;
  • 屏障任务由最后一个到达屏障的线程执行,不要在屏障任务里再次 await() 同一个屏障,否则容易死锁;
  • 如果某个线程在等待时被中断、超时,或者屏障任务抛异常,屏障可能被标记为 broken,其他等待线程会收到 BrokenBarrierException。

3. CountDownLatch 和 CyclicBarrier 的比较

特性 CountDownLatch CyclicBarrier
作用 等待其他线程完成任务或事件发生 等待所有线程到达屏障
重用性 不可重用 可重用
线程数关系 等待线程数和 countDown() 次数可以不同,一个线程可多次 countDown() 参与线程数通常固定为构造时的 parties
额外任务 不支持内置的“到齐后执行任务” 支持在屏障点执行额外任务
典型场景 主线程等待工作线程完成;资源初始化;启动信号 多线程阶段性同步;多轮并行计算
阻塞语义 等计数归零 等所有参与线程到齐

4. 选择依据

选择 CountDownLatch

  • 一次性任务:只需要等待线程完成一次操作,例如主线程等待工作线程处理完毕;
  • 等待多个资源初始化完成:初始化线程完成后通知等待线程;
  • 需要“一个线程等多个线程”或“多个线程等一个线程”:CountDownLatch 的等待关系更灵活;
  • 不需要重置计数器,也不需要所有等待线程都参与计数。

选择 CyclicBarrier

  • 多阶段任务:需要让线程在某个点同步,并反复使用该屏障;
  • 固定线程数的集合点:每次都需要固定数量的线程齐聚屏障才能继续;
  • 需要在所有线程到齐后执行一个阶段性动作,可以用 CyclicBarrier 的屏障任务。

易错点与对比

  1. CountDownLatch 不是“所有线程必须 await”

它只要求计数减到 0。可以是一个主线程 await(),多个工作线程 countDown();也可以是多个线程 await(),一个线程负责 countDown()。

  1. countDown() 建议放在 finally

如果任务执行中抛异常,没有执行 countDown(),计数器可能永远减不到 0,等待线程会一直阻塞。

  1. CountDownLatch 不能重置

计数到 0 后,后续再调用 await() 通常会立即返回;如果需要下一轮同步,必须新建一个 CountDownLatch。

  1. CyclicBarrier 的 parties 要和参与线程数匹配

如果构造时是 3,但只有 2 个线程调用 await(),屏障不会被触发,线程会一直等待,除非使用超时或发生异常。

  1. CyclicBarrier 的屏障任务由最后到达的线程执行

屏障任务不是额外开一个线程执行,而是最后一个到达屏障的线程执行。屏障任务应尽量短,且不要在里面再次等待同一个屏障。

  1. 异常会破坏屏障

CyclicBarrier.await() 可能抛出 InterruptedException、TimeoutException、BrokenBarrierException。某个线程异常后,其他线程可能被 broken 屏障唤醒并抛异常,不能简单吞掉。

  1. 两者都不是锁,也不解决共享资源竞争

CountDownLatch 和 CyclicBarrier 解决的是线程之间的等待与同步顺序,不负责保护共享变量。共享数据仍然需要锁、原子类或其他并发容器。

  1. 底层实现可以当作加分项

CountDownLatch 通常基于 AQS 的共享模式实现;CyclicBarrier 通常基于 ReentrantLock 和 Condition 实现。面试中先讲清语义和场景,再补底层实现更稳妥。

小结

  • 如果任务是一次性的,主要是等任务完成、等事件发生,用 CountDownLatch;
  • 如果任务是多阶段的,需要一组线程互相等待并且可重复同步,用 CyclicBarrier;
  • 回答这道题时,不要只说“CountDownLatch 不能重用、CyclicBarrier 能重用”,还要讲清楚:CountDownLatch 等的是计数归零,CyclicBarrier 等的是线程到齐。

面试表达可以收束为一句:两者都能用,但 CountDownLatch 适合一次性等待任务完成,CyclicBarrier 适合多线程多阶段会合;选哪个取决于同步语义和是否需要复用。

License:  CC BY 4.0