当前位置:  首页>> 技术小册>> Java并发编程实战

19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?

在Java并发编程中,确保多个线程能够按照预定的顺序或条件协同工作是一项重要而复杂的任务。CountDownLatchCyclicBarrier是Java并发包(java.util.concurrent)中提供的两个非常有用的工具类,它们各自以不同的方式帮助开发者实现线程间的同步,确保线程能够“步调一致”地执行。本章将深入探讨这两个类的使用场景、工作原理、以及它们如何帮助我们在多线程环境中控制线程的执行顺序。

1. CountDownLatch:等待直到所有任务完成

CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。CountDownLatch的工作原理类似于倒计时器:初始化时设置一个计数器(count),每当一个线程完成了它的任务,计数器的值就减一(通过调用countDown()方法)。当计数器的值达到零时,所有等待的线程(通过调用await()方法)将被唤醒继续执行。

1.1 使用场景
  • 并行计算:当需要将一个大任务拆分成多个小任务并行执行,并且需要等待所有小任务完成后才能继续执行后续操作时。
  • 资源初始化:在应用程序启动时,需要等待多个资源(如数据库连接、配置文件加载等)都准备好后才能继续执行。
1.2 示例代码
  1. import java.util.concurrent.CountDownLatch;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. public class CountDownLatchExample {
  5. public static void main(String[] args) throws InterruptedException {
  6. int numThreads = 5;
  7. CountDownLatch latch = new CountDownLatch(numThreads);
  8. ExecutorService executor = Executors.newFixedThreadPool(numThreads);
  9. for (int i = 0; i < numThreads; i++) {
  10. executor.submit(() -> {
  11. try {
  12. // 模拟任务执行
  13. Thread.sleep(1000);
  14. } catch (InterruptedException e) {
  15. Thread.currentThread().interrupt();
  16. }
  17. latch.countDown(); // 任务完成,计数器减一
  18. });
  19. }
  20. latch.await(); // 等待所有任务完成
  21. System.out.println("所有任务完成");
  22. executor.shutdown();
  23. }
  24. }

2. CyclicBarrier:让一组线程相互等待

CountDownLatch不同,CyclicBarrier允许一组线程相互等待,直到达到某个公共屏障点(common barrier point)。所有线程都必须调用await()方法,然后这些线程将在屏障处被阻塞,直到最后一个线程到达。当最后一个线程到达屏障时,所有线程将被释放,继续执行它们的await()调用之后的代码。值得注意的是,CyclicBarrier是可以重用的,即一旦所有线程都被释放,它可以被重置并再次使用,无需重新创建新的对象。

2.1 使用场景
  • 阶段性同步:在算法或任务的执行过程中,需要将执行过程分为几个阶段,每个阶段完成后才能进行下一个阶段。
  • 并行迭代:当多个线程需要迭代相同的数据集,但每个线程只能处理数据的一部分,并且在所有部分都处理完成后才能进行下一步操作。
2.2 示例代码
  1. import java.util.concurrent.BrokenBarrierException;
  2. import java.util.concurrent.CyclicBarrier;
  3. public class CyclicBarrierExample {
  4. public static void main(String[] args) {
  5. int numThreads = 4;
  6. CyclicBarrier barrier = new CyclicBarrier(numThreads, () -> {
  7. System.out.println("所有线程都到达了屏障点,继续执行后续任务");
  8. });
  9. for (int i = 0; i < numThreads; i++) {
  10. new Thread(() -> {
  11. try {
  12. // 模拟任务执行
  13. Thread.sleep(1000);
  14. System.out.println(Thread.currentThread().getName() + " 到达屏障点");
  15. barrier.await(); // 等待其他线程
  16. } catch (InterruptedException | BrokenBarrierException e) {
  17. Thread.currentThread().interrupt();
  18. }
  19. }).start();
  20. }
  21. }
  22. }

3. CountDownLatch与CyclicBarrier的比较

  • 用途差异CountDownLatch主要用于一个或多个线程等待其他多个线程完成某项操作;而CyclicBarrier则是让一组线程相互等待,直到它们都达到某个公共屏障点。
  • 重用性CountDownLatch是不可重用的,一旦计数器到达零,就不能再次被使用;而CyclicBarrier是可以重用的,每次所有线程通过屏障后,它都可以被重置并再次使用。
  • 触发条件CountDownLatch的触发条件是计数器达到零;而CyclicBarrier的触发条件是达到屏障点的线程数量与构造时指定的线程数量一致。

4. 实际应用中的注意事项

  • 死锁与性能:在多线程编程中,不恰当的同步机制可能导致死锁或性能下降。使用CountDownLatchCyclicBarrier时,应确保它们的使用不会导致线程永久阻塞或不必要的等待。
  • 异常处理:当线程在等待CyclicBarrier时,如果其中一个线程因异常退出,其他线程将抛出BrokenBarrierException。因此,在调用await()方法时,应准备好处理此异常。
  • 中断处理:在调用await()方法时,如果线程被中断,它将抛出InterruptedException。在捕获此异常后,通常需要将中断状态重新设置到当前线程,以便上层调用者能够感知到中断。

通过合理使用CountDownLatchCyclicBarrier,我们可以有效地控制多线程的执行顺序,实现复杂的并发逻辑。然而,这也要求开发者对Java并发编程有深入的理解,能够准确地把握同步机制的使用时机和方式,以避免潜在的问题。


该分类下的相关小册推荐: