MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java CyclicBarrier的实现与应用

2024-01-314.7k 阅读

Java CyclicBarrier的基本概念

在多线程编程中,经常会遇到这样的场景:多个线程需要相互等待,直到所有线程都到达某个特定点后,才能继续执行后续的操作。CyclicBarrier 就是为解决这类问题而设计的一个同步工具类。它允许一组线程互相等待,直到到达某个公共屏障点(barrier point)。

CyclicBarrier 的字面意思是“循环屏障”,“循环”表示它可以被重用。当所有参与的线程都到达屏障点后,屏障会打开,所有线程可以继续执行,并且 CyclicBarrier 可以被再次使用。这与 CountDownLatch 有所不同,CountDownLatch 是一次性的,计数器归零后就无法再次使用。

CyclicBarrier的构造函数

CyclicBarrier 提供了两个主要的构造函数:

  1. CyclicBarrier(int parties):创建一个 CyclicBarrier,它将在给定数量的线程(parties)都调用 await 方法时触发。例如:
CyclicBarrier barrier = new CyclicBarrier(3);

这里创建了一个 CyclicBarrier,需要 3 个线程调用 await 方法后,屏障才会打开。

  1. CyclicBarrier(int parties, Runnable barrierAction):除了上述功能外,当所有线程都到达屏障点时,会执行给定的 Runnable 任务。例如:
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程都已到达屏障点,执行额外任务");
});

在这个例子中,当 3 个线程都调用 await 方法到达屏障点时,会先执行 Runnable 中的代码,然后所有线程再继续执行后续操作。

CyclicBarrier的核心方法

  1. await() 方法:调用此方法的线程会阻塞,直到所有线程都调用了 await() 方法,即达到屏障点。如果当前线程是最后一个到达的线程,那么屏障会打开,所有线程会被释放并继续执行。此方法返回一个表示当前线程到达屏障点的索引值,该索引值从 parties - 1 开始递减到 0。例如:
try {
    int index = barrier.await();
    System.out.println("线程 " + Thread.currentThread().getName() + " 的索引值为 " + index);
} catch (InterruptedException | BrokenBarrierException e) {
    e.printStackTrace();
}

InterruptedException 表示线程在等待过程中被中断,BrokenBarrierException 表示屏障已经处于损坏状态(例如,在屏障打开之前,有线程被中断或超时)。

  1. await(long timeout, TimeUnit unit) 方法:与 await() 方法类似,但增加了超时机制。如果在指定的时间内没有所有线程到达屏障点,那么会抛出 TimeoutException。例如:
try {
    boolean success = barrier.await(5, TimeUnit.SECONDS);
    if (success) {
        System.out.println("所有线程在规定时间内到达屏障点");
    } else {
        System.out.println("有线程在规定时间内未到达屏障点");
    }
} catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
    e.printStackTrace();
}

CyclicBarrier的实现原理

CyclicBarrier 的实现依赖于 ReentrantLockCondition。它内部维护了一个计数器 count,初始值为 parties。每个线程调用 await() 方法时,count 会减 1。当 count 减到 0 时,表示所有线程都已到达屏障点,此时会唤醒所有等待在 Condition 上的线程,并且重置 countparties,以便下次使用。

下面是简化后的 CyclicBarrier 核心实现代码(仅为说明原理,非实际 JDK 代码):

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class SimplifiedCyclicBarrier {
    private final int parties;
    private int count;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition trip = lock.newCondition();
    private final Runnable barrierCommand;

    public SimplifiedCyclicBarrier(int parties, Runnable barrierCommand) {
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierCommand;
    }

    public int await() throws InterruptedException {
        lock.lock();
        try {
            int index = --count;
            if (index == 0) {
                if (barrierCommand != null) {
                    barrierCommand.run();
                }
                count = parties;
                trip.signalAll();
                return 0;
            } else {
                while (count != 0) {
                    trip.await();
                }
                return index;
            }
        } finally {
            lock.unlock();
        }
    }
}

在这段代码中,lock 用于保证线程安全,tripCondition 对象,用于线程的等待和唤醒。当 count 变为 0 时,执行 barrierCommand(如果有),然后重置 count 并唤醒所有等待的线程。其他线程则在 count 不为 0 时等待在 trip 上。

CyclicBarrier的应用场景

  1. 多阶段计算:在一些需要分阶段进行计算的任务中,每个阶段可能需要多个线程协同完成。例如,在一个数据分析任务中,第一阶段可能是多个线程并行读取不同数据源的数据,第二阶段是对读取到的数据进行汇总分析。在每个阶段之间,可以使用 CyclicBarrier 来确保所有线程都完成前一阶段的任务后,再开始下一阶段。
import java.util.concurrent.CyclicBarrier;

public class MultiStageComputing {
    private static final int THREADS = 3;
    private static final CyclicBarrier barrier1 = new CyclicBarrier(THREADS);
    private static final CyclicBarrier barrier2 = new CyclicBarrier(THREADS);

    public static void main(String[] args) {
        for (int i = 0; i < THREADS; i++) {
            new Thread(() -> {
                try {
                    // 第一阶段任务
                    System.out.println(Thread.currentThread().getName() + " 开始第一阶段任务");
                    // 模拟任务执行
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 完成第一阶段任务");
                    barrier1.await();

                    // 第二阶段任务
                    System.out.println(Thread.currentThread().getName() + " 开始第二阶段任务");
                    // 模拟任务执行
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 完成第二阶段任务");
                    barrier2.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这个例子中,barrier1 用于同步第一阶段任务的完成,barrier2 用于同步第二阶段任务的完成。

  1. 并发测试:在性能测试中,可能需要模拟多个并发用户同时执行某个操作,以测试系统在高并发情况下的性能。CyclicBarrier 可以用于确保所有“虚拟用户”线程同时开始执行任务。
import java.util.concurrent.CyclicBarrier;

public class ConcurrencyTesting {
    private static final int USERS = 10;
    private static final CyclicBarrier barrier = new CyclicBarrier(USERS, () -> {
        System.out.println("所有用户准备就绪,开始测试");
    });

    public static void main(String[] args) {
        for (int i = 0; i < USERS; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 准备就绪");
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() + " 开始执行测试任务");
                    // 模拟测试任务执行
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + " 完成测试任务");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

这里 CyclicBarrierbarrierAction 用于在所有“用户”线程都准备好后,打印提示信息并开始测试。

  1. 并行搜索:假设有一个大的数据集需要进行搜索,为了提高搜索效率,可以将数据集分成多个部分,每个部分由一个线程负责搜索。当所有线程完成各自部分的搜索后,再汇总结果。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CyclicBarrier;

public class ParallelSearch {
    private static final int THREADS = 4;
    private static final CyclicBarrier barrier = new CyclicBarrier(THREADS, () -> {
        System.out.println("所有搜索线程完成任务,汇总结果");
    });
    private static final List<String> dataSet = new ArrayList<>();
    private static final List<String> resultSet = new ArrayList<>();

    static {
        for (int i = 0; i < 100; i++) {
            dataSet.add("data" + i);
        }
    }

    public static void main(String[] args) {
        int partSize = dataSet.size() / THREADS;
        for (int i = 0; i < THREADS; i++) {
            int start = i * partSize;
            int end = (i == THREADS - 1)? dataSet.size() : (i + 1) * partSize;
            new Thread(() -> {
                List<String> localResult = new ArrayList<>();
                for (int j = start; j < end; j++) {
                    if (dataSet.get(j).contains("data50")) {
                        localResult.add(dataSet.get(j));
                    }
                }
                try {
                    System.out.println(Thread.currentThread().getName() + " 完成搜索任务");
                    barrier.await();
                    resultSet.addAll(localResult);
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这个例子中,每个线程负责搜索数据集的一部分,当所有线程完成搜索后,通过 barrierAction 提示开始汇总结果,并将各自的局部结果合并到 resultSet 中。

CyclicBarrier与其他同步工具的比较

  1. 与 CountDownLatch 的比较

    • 重用性CyclicBarrier 可以重用,计数器会在所有线程到达屏障点后重置;而 CountDownLatch 是一次性的,计数器归零后无法再次使用。
    • 应用场景CountDownLatch 更适用于一个或多个线程等待其他线程完成一组操作的场景,例如主线程等待所有子线程完成任务后再进行汇总。而 CyclicBarrier 更侧重于多个线程相互等待,直到所有线程都到达某个点后再继续执行。
    • 实现原理CountDownLatch 基于 AQS 框架实现,通过一个共享状态(计数器)来控制线程的等待和释放;CyclicBarrier 则依赖 ReentrantLockCondition 来实现线程的同步。
  2. 与 Semaphore 的比较

    • 功能Semaphore 主要用于控制同时访问某个资源的线程数量,它维护一个许可集合,线程获取许可才能访问资源,使用完后释放许可。而 CyclicBarrier 是用于线程间的同步,使多个线程在某个点相互等待。
    • 应用场景:如果需要限制对某个有限资源的并发访问,Semaphore 是合适的选择;如果需要多个线程协同完成某个任务,在任务的不同阶段进行同步,CyclicBarrier 更为适用。

CyclicBarrier使用中的注意事项

  1. 异常处理:在调用 await() 方法时,可能会抛出 InterruptedExceptionBrokenBarrierExceptionTimeoutException(如果使用带超时的 await 方法)。需要根据具体的业务逻辑来处理这些异常。例如,如果线程被中断,可能需要进行一些清理操作或者重新启动任务。

  2. 屏障损坏:如果在屏障打开之前,有线程被中断、超时或者调用了 reset() 方法,屏障可能会进入损坏状态。处于损坏状态的屏障会导致后续调用 await() 方法的线程抛出 BrokenBarrierException。因此,在使用 CyclicBarrier 时,要注意避免这些情况的发生,确保屏障的正常使用。

  3. 线程安全CyclicBarrier 本身是线程安全的,但是在与其他共享资源交互时,仍然需要注意线程安全问题。例如,在上述并行搜索的例子中,如果 resultSet 不是线程安全的集合,在多个线程同时向其添加元素时可能会出现数据不一致的问题。可以使用 Collections.synchronizedList 或者 CopyOnWriteArrayList 来确保线程安全。

  4. 性能问题:虽然 CyclicBarrier 可以有效地实现线程同步,但在高并发场景下,过多的线程等待和唤醒操作可能会带来性能开销。特别是当 parties 的数量较大时,需要谨慎评估性能影响。可以考虑使用更细粒度的同步机制或者优化任务的划分来减少等待时间。

通过对 CyclicBarrier 的深入了解,我们可以在多线程编程中更好地利用它来实现复杂的同步需求,提高程序的并发性能和可靠性。无论是多阶段计算、并发测试还是并行搜索等场景,CyclicBarrier 都能发挥重要作用。同时,在使用过程中要注意异常处理、屏障状态管理以及线程安全和性能等问题,以确保程序的正确性和高效性。