小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
《教父》
Never hate your enemies. If affects your judgment.
这几天把《教父》第一、二、三部电影都看了一遍,以前对这部电影早有耳闻,说是经典中的经典,男人必看的电影,我去,这几天沉下心全部看完之后,真的不愧是经典,无论是第一部中马龙白兰度饰演的第一代教父维托·唐·柯里昂,还是第二、三部阿尔·帕西诺饰演的迈克·柯里昂,都把教父这一形象表演的淋漓尽致,真的是不可多得的好电影,电影很长,但很值得一看,强烈推荐大家看一看哦。好了说完电影,进入今天的主题,一起来手撕CyclicBarrier。
CyclicBarrier,回环栅栏,它会阻塞一组线程直到这些线程同时达到某个条件才继续执行。它与CountDownLatch很类似,但又不同,CountDownLatch需要调用countDown()方法触发条件,而CyclicBarrier不需要,它就像一个栅栏一样,当一组线程都到达了栅栏处才继续往下走,而且是可重复利用的。
先来看一个简单的使用示例。
/**
* @ClassName CyclicBarrierDemo
* @Description: TODO
* @Author lirui
* @Date 2020/4/26
* @Version V1.0
**/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 3; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("都进来吧。等一等哦");
try {
cyclicBarrier.await();
Thread.sleep(3000);
System.out.println("一起释放青春把");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
});
}
}
}
// result
都进来吧。等一等哦
都进来吧。等一等哦
都进来吧。等一等哦
一起释放青春把
一起释放青春把
一起释放青春把
使用一个CyclicBarrier使得三个线程保持同步,当三个线程同时到达cyclicBarrier.await();处大家再一起往下运行。
>>>>
内部构造及属性
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;
CyclicBarrier内部维护了一个静态内部类Generation 。
Generation,中文意思是一代人的代,用于控制CyclicBarrier的循环使用。
比如,上面示例中的三个线程完成后进入下一代,继续等待三个线程达到栅栏处再一起执行,而CountDownLatch则做不到这一点,CountDownLatch是一次性的,无法重置其次数。
通过属性可以看到,CyclicBarrier内部是通过重入锁的条件锁来实现的,那么我们通过这个属性可以设想一下,假如初始时count = parties = 3,当第一个线程到达栅栏处,count减1,然后把它加入到Condition的队列中,第二个线程到达栅栏处也是如此,第三个线程到达栅栏处,count减为0,调用Condition的signalAll()通知另外两个线程,然后把它们加入到AQS的队列中,等待当前线程运行完毕,调用lock.unlock()的时候依次从AQS的队列中唤醒一个线程继续运行,也就是说实际上三个线程先依次(排队)到达栅栏处,再依次往下运行,这只是个假设,下面就来通过源码来看下是不是这样来实现的。
>>>>
await方法
public int await() throws InterruptedException, BrokenBarrierException {
try {
// 调用dowait方法,不需要指定超时
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
/**
* timed:false:不需要指定超时,true:需要指定超时
* nanos:当前需要等待的线程数
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
// 加锁
// 加锁是因为barrier的挂起和唤醒的组件是condition,
// condition需要依赖lock锁
lock.lock();
try {
// 当前代
final Generation g = generation;
// 如果当前代被打破,调用await方法的线程会抛出异常
if (g.broken)
throw new BrokenBarrierException();
// 中断检查
if (Thread.interrupted()) {
// count重置
// 唤醒trip 条件队列的所有线程
breakBarrier();
throw new InterruptedException();
}
// count值减1
int index = --count;
// 如果数量减到0,走这个if逻辑(最后一个线程走到这里)
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)
// 释放锁
// 调用condition的await方法进入条件队列尾部
// 等待被唤醒
trip.await();
else if (nanos > 0L)
//说明当前线程调用await方法是指定了超时时间的
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 何时会抛出中断异常?
//Node节点在条件队列内时收到中断信号时会抛出中断异常
// 当前代并没有变化并检查当前代是否被打破
if (g == generation && !g.broken) {
// 重置count
// 唤醒条件队列所有线程
breakBarrier();
throw ie;
} else {
//进入这里的几种情况
// 1. 代发生变化
// 2. 代没有发生变化,但代已经被打破了
//
Thread.currentThread().interrupt();
}
}
//1. 当前代被打破
// 2. 当前barrier开启了新的一代
// 等待超时
if (g.broken)
// 线程唤醒后依次抛出异常
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
private void nextGeneration() {
// 将等待队列中的线程全部唤醒
trip.signalAll();
// count值重置
count = parties;
// 开启新的一代
generation = new Generation();
}
/**
* 打破barrier屏障,在屏障内的线程都会抛出异常
*/
private void breakBarrier() {
//设置weitrue,表示这一代被打破,再来到这一代的线程会抛出异常
generation.broken = true;
// count重置
count = parties;
// 唤醒队列中的所有线程
trip.signalAll();
}
-
首先就是通过ReentrantLock进行加锁,然后拿到当前代,接着判断当前代是否被打破,被打破就抛出异常,接着判断是否被中断,被终中断的话,首先调用breakBarrier方法对count数进行重置,然后唤醒所有在条件队列的线程。
-
当count值减到为0的时候,那么就代表所有线程都到达了栅栏处,就要开始唤醒所有线程,通过调用下一代的方法nextGeneration(),通过源码可知,这个方法主要做了三件事,唤醒等待队列的线程,将count值重置,开启新的一代。
-
如果count值没有减到0,那么执行await方法的线程就要被添加进条件队列,等待被唤醒。
下面就借用网上的一张运行流程图来帮助大家更好的理解。
>>>>
总结
-
CyclicBarrier会使一组线程阻塞在await()处,当最后一个线程到达时唤醒(只是从条件队列转移到AQS队列中)前面的线程大家再继续往下走;
-
CyclicBarrier不是直接使用AQS实现的一个同步器;
-
CyclicBarrier基于ReentrantLock及其Condition实现整个同步逻辑;
cyclicBarrier与CountDownLatch的异同?
-
两者都能实现阻塞一组线程等待被唤醒;
-
前者是最后一个线程到达时自动唤醒;
-
后者是通过显式地调用countDown()实现的;
-
前者是通过重入锁及其条件锁实现的,后者是直接基于AQS实现的;
-
前者具有“代”的概念,可以重复使用,后者只能使用一次;
-
前者只能实现多个线程到达栅栏处一起运行;
-
后者不仅可以实现多个线程等待一个线程条件成立,还能实现一个线程等待多个线程条件成立
如果觉得本文章对你有用,麻烦点个赞,加个关注哦