1.是什么?
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier),类似于CountDownLatch也是个计数器,不同的是CyclicBarrier要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的(reset()方法重置屏障点)。 总结来说就是:CyclicBarrier,让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞。
2.使用场景
可以用于多线程计算数据,最后合并计算结果的场景
3.怎么用?
假设:现在餐厅要计算三个月内的盈利情况,可以分三个线程同时进行计算,最后合并结果
public class CyclicBarrier2 {
public static long m1; // 一月的工资
public static long m2; // 二月的工资
public static long m3; // 三月的工资
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2,() -> {
long all = m1 + m2 + m3;
System.out.println("三个月的工资总数为" + all);
});
for(int i = 1; i <= 3; i++){
final int n = i;
new Thread(() -> {
if(Thread.currentThread().getName().equals("thread1")){
m1 = 10000 + n*500;
System.out.println("计算第"+ n +"个月的工资:" + m1);
}else if(Thread.currentThread().getName().equals("thread2")){
m2 = 10000 + n*500;
System.out.println("计算第"+ n +"个月的工资:" + m2);
}else{
m3 = 10000 + n*500;
System.out.println("计算第"+ n +"个月的工资:" + m3);
}
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},"thread" + String.valueOf(i)).start();
}
}
}
4.底层原理
属性
public class CyclicBarrier {
// 屏障barrier是可以循环使用的,每用一次屏障都会表现为一个代(Generation)实例,里面的broken表示屏障是否被破坏,如果说一个线程被中断了,表示屏障被破坏了,需要将broken置为true,来通知后续到达的线程
private static class Generation {
boolean broken = false;
}
// 可重入锁,CyclicBarrier没有直接基于AQS框架框架去做,而是用了ReentrantLock和condition来实现线程的同步、阻塞与唤醒
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
// 触发屏障的到达线程数(到达parties个线程会触发屏障)
private final int parties;
// 达到屏障后的回调方法
private final Runnable barrierCommand;
// 当前屏障关联的代实例
private Generation generation = new Generation();
// 还需要到达count个线程才会触发屏障,每await()一次,count就减一,当减到0时,说明规定的线程全部到达,就会唤醒线程,出发屏障
private int count;
......
}
构造器
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
// 初始化parties和count,和触发屏障时的回调方法
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
方法
①await()未达到屏障,尝试将线程阻塞
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
// 该方法可以设置等待的超时时间,如果,超过了该时间依然没有到达屏障
return dowait(true, unit.toNanos(timeout));
}
②dowait(false, 0L)
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
// 上锁,也就是一个线程在尝试进入等待状态的时候,其他线程是不能进入的,保证线程安全(防止共享变量parties/countd等被修改)
lock.lock();
try {
// 获取屏障关联的代
final Generation g = generation;
// 如果屏障被破坏了,抛出异常(手动抛出异常,退出函数,异常被方法抛出,在线程调用await()时被捕获)
if (g.broken)
throw new BrokenBarrierException();
// 如果线程被中断了
if (Thread.interrupted()) {
/**
private void breakBarrier() {
// 将破坏标志设为true,一边通知后续到达的线程
generation.broken = true;
//(将count设为还没有线程到达时的初始数量)
count = parties;
// 唤醒已经阻塞的线程
trip.signalAll();
}
**/
breakBarrier();
// 抛出线程中断异常
throw new InterruptedException();
}
// 如果没有发生线程中断或者是屏障被破坏的异常情况,就将count的数量-1,表示,又有一个线程到达了
int index = --count;
// 如果,还需要达到的线程数量变为0,表示可以触发屏障了
if (index == 0) { // tripped
// 标记是否要运行runnable中的方法
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
// 如果设置了达到屏障后的回调方法,就去执行回调方法
if (command != null)
command.run();
// 如果方法运行成功,或者说没有回调方法,将ranAction置为true
ranAction = true;
/** // 重置屏障+唤醒阻塞线程
private void nextGeneration() {
// 唤醒全部阻塞的线程
trip.signalAll();
// 将count设为还没有线程到达时的初始数量
count = parties;
// 创建一个新的代对象,表示是新一轮的屏障了
generation = new Generation();
}
**/
// 如果回调方法也运行成功(或者不需要),就可以进入下一个代了
nextGeneration();
// 结束该函数,当前线程继续向下执行
return 0;
} finally {
// 如果回调方法,在执行的过程中出现了异常,也表示屏障被破坏了,需要
// 修改屏障破坏标志true,通知后续线程,唤醒已经阻塞了的线程
if (!ranAction)
breakBarrier();
}
}
// 如果,还需要达到的线程数量不为0,表示还没有到达同步点
for (;;) {
try {
// 判断是否设置了超时
if (!timed)
// 如果没有设置超时时间,直接将将当前线程阻塞
trip.await();
// 如果设置了超时时间,并且超时时间>0(有意义)
else if (nanos > 0L)
// 获取超时时间和已经过去了的时间差(代码比较复杂,并且不是讨论的重点了,不做分析)
// <= 0 ,表明发生了超时,> 0,都没有超时
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 如果在获取时间差的时候,当前线程被中断了,并且还没有进入下一代,屏障也没有被破坏,需要执行breakBarrier()↑,修改屏障破坏标志true,表示屏障被破坏了,并抛出异常结束方法
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// 否则,直接中断当前线程
Thread.currentThread().interrupt();
}
}
// 能到达此处,可能是阻塞的线程被唤醒了(可能因为正常到达屏障、也可能因为某个线程被中断了),或者设置了超时时间
// 如果是屏障被破坏了,直接抛出异常,结束当前方法(方法不会继续执行)
if (g.broken)
throw new BrokenBarrierException();
// 如果已经是下一代了,表明达到了屏障,线程可以继续执行了,返回index,(parties-index)是线程的到达次序(表明该线程是第几个到达的)
if (g != generation)
return index;
// 如果发生了超时
if (timed && nanos <= 0L) {
// 标记屏障被破坏-->通知其他线程,唤醒阻塞线程,修改屏障破坏标志true,
breakBarrier();
// 抛出超时异常,
throw new TimeoutException();
}
}
} finally {
// 解锁
lock.unlock();
}
}
什么情况下线程会被唤醒? 如果该线程不是最后一个调用await()方法的线程,则它会一直处于等待状态,除非发生以下情况:
-
最后一个线程到达,即index = 0。
-
某个参与线程等待超时。
-
某个参与线程被中断。(避免了因为一个线程中断引起永远不能到达屏障点而导致其他线程一直等待)
-
调用了CyclicBarrier的reset()方法,将屏障重置为初始状态。
上述后三种情况都会唤醒所有阻塞的线程,并抛出BrokenBarrierException异常
一旦屏障被破坏了,就不可以使用了(broken=true),除非调用reset()进行重置
(重要)
Countdownlatch 和 Cyclicbarrier 区别?
1.使用场景上:
CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再能被唤醒继续执行
CountDownLatch:相当于一个计数器,一个或者多个线程,等待其他多个线程完成某件事情之后才能执行
2.在实现上:
CyclicBarrier它没有直接实现AQS框架而是借助ReentrantLock和condition来实现的同步机制
CountDownLatch是AQS框架共享模式的直接实现
3.在使用上:
CyclicBarrier是可以循环使用的,触发屏障或者重置屏障,都会进入下一代,唤醒阻塞线程,重置count和generation属性,保证它可以继续使用
Countdownlatch是一次性的,一个线程完成某些操作后将同步状态state减一,减到0,await()阻塞的线程才会继续执行,同步状态state不会被重置
4.CyclicBarrier中一个线程被中断,其他线程都会抛出异常
Countdownlatch中一个await()的线程被中断,其他线程被依次唤醒后继续执行
5.创建CyclicBarrier对象的时候,能够接受一个Runnable参数,达到屏障后,方法被调用,
Countdownlatch不具备这个功能
6.CyclicBarrier方法有返回值,可以通过它的返回值计算出它是第几个到达的线程
Countdownlatch的await()方法没有返回值
如有问题欢迎指正
原文链接:【细谈Java并发】谈谈CyclicBarrier - 简书