是什么?
循环屏障,顾名思义像一个屏障那样,可以拦截一定数量的线程,当足够数量多的线程到达屏障后,就会“突破”屏障,所有被拦截的线程会继续往下执行,也类似于一个“集合点”“计数器”,不过它可以被循环利用
怎么用?
例子1:
举一个简单的例子,学校开校运会,所有学生站上赛道后才能开跑
public class School {
private static CyclicBarrier barrier = new CyclicBarrier(3);
public static void main(String[] args) {
// 把这个线程池想像成是赛道场地
ExecutorService executor = Executors.newFixedThreadPool(3);
// ABC 三名学生站上了赛道
executor.execute(new Thread(new Student("A")));
executor.execute(new Thread(new Student("B")));
executor.execute(new Thread(new Student("C")));
// 关闭赛道
executor.shutdown();
}
static class Student implements Runnable {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " Ready");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(name + " Run!");
}
}
}
执行结果:
可以看到barrier可以想象成一个栅栏,等所有学生就位后,挪开栅栏,学生们才开始下一步起跑。
有一点需要注意的是,我们设置了三个学生才开跑,如果只有两个学生,那么比赛将会一直卡着不开始,例如我把C学生注释掉,执行结果就变成了:
例子二:
ABC三位同学跑步完毕,下一项是游泳,一样是等三个人同时准备好才能开始游:
public class School {
private static CyclicBarrier barrier = new CyclicBarrier(3);
public static void main(String[] args) {
// 把这个线程池想像成是赛道场地
ExecutorService executor = Executors.newFixedThreadPool(3);
// ABC 三名学生站上了赛道参加跑步比赛
executor.execute(new Thread(new StudentRun("A")));
executor.execute(new Thread(new StudentRun("B")));
executor.execute(new Thread(new StudentRun("C")));
// ABC 三名学生站上了赛道参加游泳比赛
executor.execute(new Thread(new StudentSwim("A")));
executor.execute(new Thread(new StudentSwim("B")));
executor.execute(new Thread(new StudentSwim("C")));
// 关闭赛道
executor.shutdown();
}
static class StudentRun implements Runnable {
private String name;
public StudentRun(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " Ready");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(name + " Run!");
}
}
static class StudentSwim implements Runnable {
private String name;
public StudentSwim(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " Ready");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(name + " Swim!");
}
}
}
这个就是CyclicBarrier的循环复用性,它就像一个设置好的坑位,一个线程的统一起跑线,可以重复被使用
注意⚠️:
Barrier只是一个类似计数器的作用,它只知道三个人参加项目,但是具体什么项目它是一概不管的,例如下面的例子
AB跑步,A又游泳,barrier识别不出来A重复报名和游泳跑步不能揉杂在一起进行,它傻乎乎的觉得够三个人了,那就比赛开始吧!
于是就乱套了
例子三:
假如校运会有规定,迟到的最后一个罚款200并且当众道歉,那么我们可以定义barrier的时候同时定义“规定“,代码改成:
public class School {
private static CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "同学道歉,并且缴纳了200罚款!");
}
});
public static void main(String[] args) {
// 把这个线程池想像成是赛道场地
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(new Thread(new StudentRun()));
executor.execute(new Thread(new StudentRun()));
executor.execute(new Thread(new StudentRun()));
// 关闭赛道
executor.shutdown();
}
static class StudentRun implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " Ready");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " Run!");
}
}
}
例子4:
如果3号选手确实不来了,摔断腿了或者睡过头了,比赛还是要开始,这个时候要设置一个barrier.await的最大等待时间,比如说等三秒,线程3还不来,那就直接开跑
执行结果:
注意这个BrokenBarrierException异常,它会打破屏障,让线程停止等待。
源码
先从没什么好说的new方法,跟很多工具类一样,new只是设置了一些初始化参数,方便稍后计数器做线程数统计和额外方法执行
那么为什么一个await方法就能干这么多事情,继续看下源码:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock; // 防止并发问题,锁当前操作
lock.lock();
try {
final Generation g = generation; // 获取当前代的状态
// 判断是否中断,如果中断就抛出屏障Broken异常,所有线程不受管控继续往下走
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// count就是我们设置的屏障拦截线程数
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 判断是否有配置额外方法需要执行,也就是我们例二中配置的最后一个线程进来时候做的事情
final Runnable command = barrierCommand;
if (command != null)
// 有的话就执行
command.run();
ranAction = true;
// 然后开始下一代,也就是重置屏障
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// 如果屏障数还未达到要求,一直循环,直到中断,或者超时
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
// 调用await方法进行线程阻塞
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
而那个nextGeneration,就是循环屏障的关键,它是进行重置计数器参数的作用
最后看bewakBarrier
把当前代设置为打破,并唤醒所有线程,同时重置count屏障计数器
CyclicBarrier与CountDownLatch的区别
(1)CountDownLatch是阻塞主线程进行等待,CyclicBarrier是阻塞子线程
(2)CountDownLatch不可重用,计数器归零就不可以再用了
(3)场景不同,CountDownLatch是等N个子线程都做完什么事后,某个线程再去做一件事,而CyclicBarrier是当N个子线程都做完什么事后,N个子线程再一起做什么事
总结
说实话,实际业务场景中,一次都没用过,所以本人的使用也局限于科普式使用,但是有些小场景,例如多线程采集小批次数据流入,1000个线程采集为一小批,或者10分钟await为一小批,排序,比较大小,然后再进行处理,就可以采用Barrier更优雅处理(起码比定时器更简洁高效精准)
等真的有场景用上了,会持续补充…