字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
CyclicBarrier的使用
构造方法
// parties表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
public CyclicBarrier(int parties)
// 用于在线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景(该线程的执行时机是在到达屏障之后再执行)
重要方法
//屏障 指定数量的线程全部调用await()方法时,这些线程不再阻塞
// BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
//循环 通过reset()方法可以进行重置
CyclicBarrier应用场景
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的场景。
import java.util.Set;
import java.util.concurrent.*;
public class CyclicBarrierTest2 {
//保存每个学生的平均成绩
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
private ExecutorService threadPool = Executors.newFixedThreadPool(3);
private CyclicBarrier cb = new CyclicBarrier(3, () -> {
int result = 0;
Set<String> set = map.keySet();
for (String s : set) {
result += map.get(s);
}
System.out.println("三人平均成绩为:" + (result / 3) + "分");
});
public void count() {
for (int i = 0; i < 3; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
//获取学生平均成绩
int score = (int) (Math.random() * 40 + 60);
map.put(Thread.currentThread().getName(), score);
System.out.println(Thread.currentThread().getName()
+ "同学的平均成绩为:" + score);
try {
//执行完运行await(),等待所有学生平均成绩都计算完毕
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
}
}
public static void main(String[] args) {
CyclicBarrierTest2 cb = new CyclicBarrierTest2();
cb.count();
}
}
执行结果如下
pool-1-thread-1同学的平均成绩为:96
pool-1-thread-2同学的平均成绩为:72
pool-1-thread-3同学的平均成绩为:61
三人平均成绩为:76分
利用CyclicBarrier的计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class CyclicBarrierTest3 {
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 5, 1000, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
(r) -> new Thread(r, counter.addAndGet(1) + " 号 "),
new ThreadPoolExecutor.AbortPolicy());
CyclicBarrier cyclicBarrier = new CyclicBarrier(5,
() -> System.out.println("裁判:比赛开始~~"));
for (int i = 0; i < 10; i++) {
threadPoolExecutor.submit(new Runner(cyclicBarrier));
}
}
static class Runner extends Thread {
private CyclicBarrier cyclicBarrier;
public Runner(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
int sleepMills = ThreadLocalRandom.current().nextInt(1000);
Thread.sleep(sleepMills);
System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms" + cyclicBarrier.getNumberWaiting());
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
执行结果如下
5 号 选手已就位, 准备共用时: 537ms0
2 号 选手已就位, 准备共用时: 661ms1
4 号 选手已就位, 准备共用时: 717ms2
1 号 选手已就位, 准备共用时: 745ms3
3 号 选手已就位, 准备共用时: 800ms4
裁判:比赛开始~~
4 号 选手已就位, 准备共用时: 634ms0
1 号 选手已就位, 准备共用时: 685ms1
2 号 选手已就位, 准备共用时: 721ms2
5 号 选手已就位, 准备共用时: 841ms3
3 号 选手已就位, 准备共用时: 926ms4
裁判:比赛开始~~
CyclicBarrier源码分析
关注点:
1.一组线程在触发屏障之前互相等待,最后一个线程到达屏障后唤醒逻辑是如何实现的
2.删栏循环使用是如何实现的
3.条件队列到同步队列的转换实现逻辑
从上图不难看出,CyclicBarrier的功能主要是靠ReentrantLock来实现的。调用java.util.concurrent.CyclicBarrier#await()方法的时候,线程开始等待,直到parties数量的线程都到达。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
此处dowait就是等待的逻辑。
/**
* Main barrier code, covering the various policies.
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
首先获取锁
lock.lock();
try {
final Generation g = generation;
Generation在构造出来之后除非调用breakBarrier方法,否则都是false
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
count随着调用dowait方法而减少 如果所有线程都到达 那么index就为0,此时就会进入到下一轮并调用nextGeneration方法。
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();
}
}
如果还有线程没有到达,index不为0.则线程会进入到条件队列进行等待。(进入条件队列等待会释放锁资源)
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
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方法如下
/**
* Updates state on barrier trip and wakes up everyone.
* Called only while holding lock.
*/
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
在这里可以看到除了重新设置count值和重新构造Generation之外,最重要的就是通过signalAll来唤醒条件队列中等待的线程。也就是在上面dowait方法中等待的线程。
从上面的分析不难看出,其实CyclicBarrier的主要逻辑都是在dowait方法当中,调用一次dowait方法,count值就减少1,只要还没有减少到0,就会进入到条件队列中等待,一旦减少到零的时候,就会唤醒条件队列中的所有线程。
更多细节参考下图
https://www.processon.com/view/link/6197b0aef346fb271b36a2bf