揭秘 Java CyclicBarrier:深入剖析其使用原理与源码实现
一、引言
在 Java 并发编程的世界里,线程之间的同步与协作是至关重要的。为了实现这一目标,Java 提供了一系列强大的并发工具类,其中 CyclicBarrier 就是一个非常实用的同步工具。CyclicBarrier 允许一组线程相互等待,直到所有线程都到达某个公共屏障点(也称为同步点),然后这些线程才会继续执行后续的操作。这个工具在多线程计算、并行任务处理等场景中有着广泛的应用。
本文将从源码层面深入剖析 Java CyclicBarrier 的使用原理,详细解释其每一个步骤的实现细节。通过对源码的解读,我们可以更好地理解 CyclicBarrier 的工作机制,从而在实际开发中更加灵活、高效地使用它。
二、CyclicBarrier 概述
2.1 基本概念
CyclicBarrier,从字面上理解,就是“循环屏障”的意思。它的主要作用是让一组线程在到达某个屏障点时相互等待,直到所有线程都到达该屏障点,然后这些线程可以继续执行后续的操作。而且,CyclicBarrier 是可循环使用的,也就是说,在一次同步操作完成后,它可以被重置,用于下一次的同步。
2.2 核心方法
CyclicBarrier 提供了几个核心方法,下面我们来简单介绍一下:
CyclicBarrier(int parties):构造函数,用于创建一个 CyclicBarrier 实例,其中parties参数表示参与同步的线程数量。CyclicBarrier(int parties, Runnable barrierAction):构造函数,除了指定参与同步的线程数量外,还可以指定一个在所有线程到达屏障点后执行的任务barrierAction。int await():当前线程调用该方法后会被阻塞,直到所有参与同步的线程都调用了该方法,即所有线程都到达了屏障点。该方法会返回当前线程到达屏障点的顺序号。int await(long timeout, TimeUnit unit):与await()方法类似,但可以指定一个超时时间。如果在超时时间内还有线程未到达屏障点,当前线程会抛出TimeoutException异常。boolean isBroken():用于检查 CyclicBarrier 是否处于损坏状态。如果有线程在等待过程中被中断或超时,CyclicBarrier 会进入损坏状态。void reset():将 CyclicBarrier 重置为初始状态,以便进行下一次的同步操作。
2.3 简单示例
下面是一个简单的示例,展示了 CyclicBarrier 的基本使用:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
// 创建一个 CyclicBarrier 实例,指定参与同步的线程数量为 3
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
// 当所有线程都到达屏障点后,执行该任务
System.out.println("所有线程都已到达屏障点,继续执行后续操作...");
});
// 创建并启动三个线程
for (int i = 0; i < 3; i++) {
new Thread(new Worker(barrier), "Thread-" + i).start();
}
}
static class Worker implements Runnable {
private final CyclicBarrier barrier;
public Worker(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 正在执行任务...");
// 模拟任务执行时间
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 已到达屏障点");
// 调用 await 方法,等待其他线程到达屏障点
int index = barrier.await();
System.out.println(Thread.currentThread().getName() + " 继续执行,到达顺序号为:" + index);
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
在这个示例中,我们创建了一个 CyclicBarrier 实例,指定参与同步的线程数量为 3,并指定了一个在所有线程到达屏障点后执行的任务。然后,我们创建并启动了三个线程,每个线程在执行完任务后调用 await() 方法等待其他线程。当所有线程都到达屏障点后,会执行指定的任务,然后所有线程继续执行后续的操作。
三、CyclicBarrier 源码结构分析
3.1 类定义与成员变量
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class CyclicBarrier {
// 表示一个代,每次屏障被触发或重置时,会创建一个新的代
private static class Generation {
// 标记屏障是否损坏
boolean broken = false;
}
// 用于保护屏障的重入锁
private final ReentrantLock lock = new ReentrantLock();
// 用于线程等待的条件变量
private final Condition trip = lock.newCondition();
// 参与同步的线程数量
private final int parties;
// 当所有线程都到达屏障点后执行的任务
private final Runnable barrierCommand;
// 当前代
private Generation generation = new Generation();
// 还需要等待的线程数量
private int count;
// 构造函数,指定参与同步的线程数量
public CyclicBarrier(int parties) {
this(parties, null);
}
// 构造函数,指定参与同步的线程数量和屏障动作
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
}
从上述源码可以看出,CyclicBarrier 类包含了几个重要的成员变量:
Generation类:这是一个内部静态类,用于表示一个代。每次屏障被触发或重置时,会创建一个新的代。broken字段用于标记屏障是否损坏。lock:一个ReentrantLock类型的锁,用于保护屏障的状态,确保线程安全。trip:一个Condition类型的条件变量,用于线程的等待和唤醒操作。parties:表示参与同步的线程数量,在构造函数中初始化。barrierCommand:一个Runnable类型的任务,在所有线程都到达屏障点后执行。generation:当前代的实例,初始化为一个新的Generation对象。count:还需要等待的线程数量,初始值为parties。
3.2 构造函数分析
CyclicBarrier 提供了两个构造函数:
// 构造函数,指定参与同步的线程数量
public CyclicBarrier(int parties) {
// 调用另一个构造函数,屏障动作传入 null
this(parties, null);
}
// 构造函数,指定参与同步的线程数量和屏障动作
public CyclicBarrier(int parties, Runnable barrierAction) {
// 检查参与同步的线程数量是否小于等于 0,如果是则抛出异常
if (parties <= 0) throw new IllegalArgumentException();
// 初始化参与同步的线程数量
this.parties = parties;
// 初始化还需要等待的线程数量
this.count = parties;
// 初始化屏障动作
this.barrierCommand = barrierAction;
}
第一个构造函数调用了第二个构造函数,并将屏障动作 barrierAction 传入 null。第二个构造函数会检查传入的 parties 参数是否小于等于 0,如果是则抛出 IllegalArgumentException 异常。然后,它会初始化 parties、count 和 barrierCommand 这三个成员变量。
四、核心方法源码分析
4.1 await() 方法
// 无超时时间的 await 方法
public int await() throws InterruptedException, BrokenBarrierException {
try {
// 调用带超时时间的 await 方法,超时时间为无限大
return dowait(false, 0L);
} catch (TimeoutException toe) {
// 由于超时时间为无限大,这里不会抛出 TimeoutException 异常
throw new Error(toe);
}
}
// 带超时时间的 await 方法
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
// 调用 dowait 方法,传入是否有超时时间的标志和转换后的超时时间
return dowait(true, unit.toNanos(timeout));
}
// 核心等待方法
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 获取锁,确保线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取当前代
final Generation g = generation;
// 检查屏障是否已损坏,如果是则抛出 BrokenBarrierException 异常
if (g.broken)
throw new BrokenBarrierException();
// 检查当前线程是否被中断,如果是则打破屏障并抛出 InterruptedException 异常
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 还需要等待的线程数量减 1
int index = --count;
// 如果还需要等待的线程数量为 0,说明所有线程都已到达屏障点
if (index == 0) {
boolean ranAction = false;
try {
// 获取屏障动作
final Runnable command = barrierCommand;
if (command != null) {
// 执行屏障动作
command.run();
}
ranAction = true;
// 触发下一代
nextGeneration();
return 0;
} finally {
if (!ranAction)
// 如果屏障动作执行失败,则打破屏障
breakBarrier();
}
}
// 循环等待,直到所有线程都到达屏障点或出现异常
for (;;) {
try {
if (!timed)
// 无超时时间的等待
trip.await();
else if (nanos > 0L)
// 带超时时间的等待
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
// 如果当前代未改变且屏障未损坏,则打破屏障并抛出 InterruptedException 异常
breakBarrier();
throw ie;
} else {
// 否则,重新设置中断状态
Thread.currentThread().interrupt();
}
}
// 检查屏障是否已损坏,如果是则抛出 BrokenBarrierException 异常
if (g.broken)
throw new BrokenBarrierException();
// 检查是否进入了下一代,如果是则返回当前线程的到达顺序号
if (g != generation)
return index;
// 如果是带超时时间的等待且超时时间已到,则打破屏障并抛出 TimeoutException 异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
// 释放锁
lock.unlock();
}
}
await() 方法是 CyclicBarrier 的核心方法,用于让线程等待其他线程到达屏障点。它有两个重载版本,一个是无超时时间的 await() 方法,另一个是带超时时间的 await(long timeout, TimeUnit unit) 方法。这两个方法最终都会调用 dowait(boolean timed, long nanos) 方法来实现具体的等待逻辑。
在 dowait 方法中,首先会获取锁,确保线程安全。然后,它会检查当前屏障是否已损坏或当前线程是否被中断,如果是则进行相应的处理。接着,将还需要等待的线程数量减 1,如果减到 0 说明所有线程都已到达屏障点,此时会执行屏障动作(如果有的话),并触发下一代。如果还需要等待的线程数量不为 0,则线程会进入循环等待状态,直到所有线程都到达屏障点或出现异常。在等待过程中,线程会根据是否有超时时间选择不同的等待方式。如果在等待过程中线程被中断或超时,会进行相应的处理并抛出异常。最后,释放锁。
4.2 breakBarrier() 方法
// 打破屏障的方法
private void breakBarrier() {
// 将当前代的 broken 标志设置为 true,表示屏障已损坏
generation.broken = true;
// 重置还需要等待的线程数量
count = parties;
// 唤醒所有等待的线程
trip.signalAll();
}
breakBarrier() 方法用于打破屏障,当有线程在等待过程中被中断或超时,会调用该方法。它会将当前代的 broken 标志设置为 true,表示屏障已损坏,然后重置还需要等待的线程数量为 parties,最后唤醒所有等待的线程。
4.3 nextGeneration() 方法
// 触发下一代的方法
private void nextGeneration() {
// 唤醒所有等待的线程
trip.signalAll();
// 重置还需要等待的线程数量
count = parties;
// 创建一个新的代
generation = new Generation();
}
nextGeneration() 方法用于触发下一代,当所有线程都到达屏障点后,会调用该方法。它会唤醒所有等待的线程,重置还需要等待的线程数量为 parties,并创建一个新的代。
4.4 isBroken() 方法
// 检查屏障是否已损坏的方法
public boolean isBroken() {
// 获取锁,确保线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 返回当前代的 broken 标志
return generation.broken;
} finally {
// 释放锁
lock.unlock();
}
}
isBroken() 方法用于检查屏障是否已损坏,它会获取锁,然后返回当前代的 broken 标志,最后释放锁。
4.5 reset() 方法
// 重置屏障的方法
public void reset() {
// 获取锁,确保线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 打破当前屏障
breakBarrier();
// 触发下一代
nextGeneration();
} finally {
// 释放锁
lock.unlock();
}
}
reset() 方法用于将 CyclicBarrier 重置为初始状态,以便进行下一次的同步操作。它会先调用 breakBarrier() 方法打破当前屏障,然后调用 nextGeneration() 方法触发下一代。
五、CyclicBarrier 的使用场景分析
5.1 多线程计算任务
在多线程计算任务中,CyclicBarrier 可以用于让多个线程并行计算,然后在所有线程都完成计算后进行汇总。例如,假设有一个大数组需要进行分段求和,我们可以将数组分成多个小段,每个线程负责计算一段的和,然后在所有线程都完成计算后,将各个小段的和汇总得到最终结果。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class MultiThreadedCalculation {
private static final int ARRAY_SIZE = 1000;
private static final int THREAD_COUNT = 4;
private static final int[] array = new int[ARRAY_SIZE];
private static final int[] partialSums = new int[THREAD_COUNT];
private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
// 所有线程都完成计算后,进行汇总
int totalSum = 0;
for (int partialSum : partialSums) {
totalSum += partialSum;
}
System.out.println("数组的总和为:" + totalSum);
});
public static void main(String[] args) {
// 初始化数组
for (int i = 0; i < ARRAY_SIZE; i++) {
array[i] = i + 1;
}
// 计算每个线程负责的数组段的起始和结束索引
int segmentSize = ARRAY_SIZE / THREAD_COUNT;
for (int i = 0; i < THREAD_COUNT; i++) {
int start = i * segmentSize;
int end = (i == THREAD_COUNT - 1)? ARRAY_SIZE : (i + 1) * segmentSize;
new Thread(new Worker(i, start, end)).start();
}
}
static class Worker implements Runnable {
private final int threadIndex;
private final int start;
private final int end;
public Worker(int threadIndex, int start, int end) {
this.threadIndex = threadIndex;
this.start = start;
this.end = end;
}
@Override
public void run() {
try {
// 计算当前线程负责的数组段的和
int sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
partialSums[threadIndex] = sum;
System.out.println("线程 " + threadIndex + " 计算的部分和为:" + sum);
// 等待其他线程完成计算
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
在这个示例中,我们创建了一个包含 1000 个元素的数组,将其分成 4 个小段,每个线程负责计算一段的和。在所有线程都完成计算后,会执行屏障动作,将各个小段的和汇总得到最终结果。
5.2 并行任务处理
在并行任务处理中,CyclicBarrier 可以用于让多个线程并行执行不同的任务,然后在所有任务都完成后进行下一步操作。例如,假设有一个系统需要同时从多个数据源获取数据,然后对这些数据进行整合和分析。我们可以使用多个线程并行地从不同的数据源获取数据,然后在所有线程都获取到数据后,进行数据的整合和分析。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class ParallelTaskProcessing {
private static final int TASK_COUNT = 3;
private static final CyclicBarrier barrier = new CyclicBarrier(TASK_COUNT, () -> {
// 所有任务都完成后,进行数据整合和分析
System.out.println("所有任务都已完成,开始进行数据整合和分析...");
});
public static void main(String[] args) {
for (int i = 0; i < TASK_COUNT; i++) {
new Thread(new Task(i)).start();
}
}
static class Task implements Runnable {
private final int taskIndex;
public Task(int taskIndex) {
this.taskIndex = taskIndex;
}
@Override
public void run() {
try {
System.out.println("任务 " + taskIndex + " 开始执行...");
// 模拟任务执行时间
Thread.sleep(2000);
System.out.println("任务 " + taskIndex + " 执行完毕");
// 等待其他任务完成
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
在这个示例中,我们创建了 3 个任务,每个任务由一个线程执行。在所有任务都完成后,会执行屏障动作,进行数据的整合和分析。
六、CyclicBarrier 的异常处理
6.1 BrokenBarrierException
当 CyclicBarrier 处于损坏状态时,调用 await() 方法的线程会抛出 BrokenBarrierException 异常。例如,当有线程在等待过程中被中断或超时,CyclicBarrier 会进入损坏状态,此时其他线程调用 await() 方法就会抛出该异常。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class BrokenBarrierExceptionExample {
private static final CyclicBarrier barrier = new CyclicBarrier(2);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
System.out.println("线程 1 正在等待...");
barrier.await();
System.out.println("线程 1 继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
System.out.println("线程 1 抛出异常:" + e.getClass().getSimpleName());
}
});
Thread thread2 = new Thread(() -> {
try {
System.out.println("线程 2 正在等待...");
// 中断线程 2
Thread.currentThread().interrupt();
barrier.await();
System.out.println("线程 2 继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
System.out.println("线程 2 抛出异常:" + e.getClass().getSimpleName());
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,线程 2 在等待过程中被中断,CyclicBarrier 会进入损坏状态,此时线程 1 调用 await() 方法会抛出 BrokenBarrierException 异常。
6.2 InterruptedException
当线程在等待过程中被中断时,会抛出 InterruptedException 异常。例如,在上述示例中,线程 2 被中断后会抛出该异常。
6.3 TimeoutException
当使用带超时时间的 await(long timeout, TimeUnit unit) 方法时,如果在超时时间内还有线程未到达屏障点,当前线程会抛出 TimeoutException 异常。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class TimeoutExceptionExample {
private static final CyclicBarrier barrier = new CyclicBarrier(2);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
System.out.println("线程 1 正在等待...");
// 设置超时时间为 1 秒
barrier.await(1, TimeUnit.SECONDS);
System.out.println("线程 1 继续执行");
} catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
System.out.println("线程 1 抛出异常:" + e.getClass().getSimpleName());
}
});
Thread thread2 = new Thread(() -> {
try {
System.out.println("线程 2 开始执行任务...");
// 模拟任务执行时间为 2 秒,超过了线程 1 的超时时间
Thread.sleep(2000);
System.out.println("线程 2 执行完毕,正在等待...");
barrier.await();
System.out.println("线程 2 继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
System.out.println("线程 2 抛出异常:" + e.getClass().getSimpleName());
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,线程 1 设置了 1 秒的超时时间,而线程 2 的任务执行时间为 2 秒,超过了线程 1 的超时时间,因此线程 1 会抛出 TimeoutException 异常。
七、CyclicBarrier 与其他同步工具的比较
7.1 与 CountDownLatch 的比较
7.1.1 功能差异
- CyclicBarrier:允许一组线程相互等待,直到所有线程都到达某个公共屏障点,然后这些线程可以继续执行后续的操作。它可以循环使用,适用于需要多次同步的场景。
- CountDownLatch:允许一个或多个线程等待其他线程完成一组操作。它的计数器只能递减一次,不能重置,适用于一次性同步的场景。
7.1.2 实现原理差异
- CyclicBarrier:基于
ReentrantLock和Condition实现,通过count变量记录还需要等待的线程数量,当count减到 0 时,触发屏障动作并唤醒所有等待的线程。 - CountDownLatch:基于
AbstractQueuedSynchronizer(AQS)实现,通过state变量记录计数器的值,当state减到 0 时,唤醒所有等待的线程。
7.1.3 使用场景差异
- CyclicBarrier:适用于多线程计算、并行任务处理等需要多次同步的场景。
- CountDownLatch:适用于主线程等待多个子线程完成任务后再继续执行的场景,如等待多个线程完成数据加载后进行数据处理。
7.2 与 Semaphore 的比较
7.2.1 功能差异
- CyclicBarrier:主要用于线程间的同步,让一组线程在到达某个屏障点时相互等待,直到所有线程都到达该屏障点。
- Semaphore:用于控制同时访问某个资源的线程数量,通过获取和释放信号量来实现。
7.2.2 实现原理差异
- CyclicBarrier:基于
ReentrantLock和Condition实现,通过count变量记录还需要等待的线程数量,当count减到 0 时,触发屏障动作并唤醒所有等待的线程。 - Semaphore:基于
AbstractQueuedSynchronizer(AQS)实现,通过state变量记录信号量的数量,线程通过调用acquire()方法获取信号量,调用release()方法释放信号量。
7.2.3 使用场景差异
- CyclicBarrier:适用于多线程计算、并行任务处理等需要线程间同步的场景。
- Semaphore:适用于资源有限的场景,如限制同时访问数据库连接池的线程数量。
八、总结与展望
8.1 总结
通过对 Java CyclicBarrier 的源码分析和使用场景介绍,我们可以总结出以下几点:
- 功能强大:CyclicBarrier 是一个非常实用的同步工具,它允许一组线程相互等待,直到所有线程都到达某个公共屏障点,然后这些线程可以继续执行后续的操作。它还可以循环使用,适用于需要多次同步的场景。
- 实现原理清晰:CyclicBarrier 基于
ReentrantLock和Condition实现,通过count变量记录还需要等待的线程数量,当count减到 0 时,触发屏障动作并唤醒所有等待的线程。同时,它使用Generation类来表示代,确保每次屏障被触发或重置时,会创建一个新的代。 - 异常处理完善:CyclicBarrier 提供了完善的异常处理机制,当线程在等待过程中被中断、超时或屏障处于损坏状态时,会抛出相应的异常,如
InterruptedException、BrokenBarrierException和TimeoutException。 - 使用场景广泛:CyclicBarrier 在多线程计算、并行任务处理等场景中有着广泛的应用,可以提高程序的并发性能和效率。
8.2 展望
随着 Java 技术的不断发展和应用场景的不断扩展,CyclicBarrier 可能会在以下几个方面得到进一步的优化和改进:
- 性能优化:在高并发场景下,CyclicBarrier 的性能可能会受到一定的影响。未来可以通过优化锁的使用、减少线程上下文切换等方式来提高其性能。
- 功能扩展:可以考虑为 CyclicBarrier 增加一些新的功能,如支持动态调整参与同步的线程数量、支持不同类型的屏障动作等,以满足更多的应用场景需求。
- 与其他并发工具的集成:可以将 CyclicBarrier 与其他并发工具(如
ExecutorService、CompletableFuture等)进行集成,提供更强大的并发编程能力。
总之,CyclicBarrier 作为 Java 并发编程中的一个重要工具,在未来的发展中有着广阔的前景。通过不断的优化和改进,它将为开发者提供更加高效、灵活的同步解决方案。
以上是一篇关于 Java CyclicBarrier 使用原理的技术博客,希望能满足你的需求。如果你对内容还有其他要求,比如某个部分需要进一步展开分析等,可以随时告诉我。