一、简介
前面已经简单介绍了CountDownLatch闭锁,本文的CyclicBarrier其实跟闭锁差不多,当然还是存在一些区别。
官网介绍如下:
CyclicBarrier是一种同步辅助工具,允许一组线程彼此等待到达一个共同的障碍点。CyclicBarrier在包含固定大小的线程的程序中非常有用,这些线程有时必须彼此等待。这个屏障被称为循环屏障,因为它可以在等待的线程被释放后重新使用。
CyclicBarrier支持一个可选的Runnable命令,该命令在在最后一个线程到达之后,但是在释放任何线程之前执行,这个屏障动作对于在任何一方继续之前更新共享状态非常有用。
通俗理解,就是CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候,所有因调用await方法而被阻塞的线程将被唤醒。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用(栅栏可重复使用)。
二、常见API
- 构造方法:
//创建一个新的CyclicBarrier,当给定数量的参与方(线程)正在等待它时,它将会跳闸,并且在跳闸时不会执行预定义的操作。
CyclicBarrier(int parties)
//创建一个新的CyclicBarrier,当给定数量的参与方(线程)正在等待它时,它将跳闸,当屏障跳闸时,它将执行给定的barrier动作,由最后一个进入屏障的线程执行。
CyclicBarrier(int parties, Runnable barrierAction)
- 常见方法:
int |
await() 等待,直到所有参与方都已调用此屏障上的等待。 |
int |
await(long timeout, TimeUnit unit) 等待,直到所有参与方都已调用此屏障上的wait,或指定的等待时间已过。 |
int |
返回当前在屏障处等待的参与方的数量 |
int |
返回需要跨越此屏障的参与方的数量。 |
boolean |
isBroken() 查询此屏障是否处于中断状态。 |
void |
reset() 将屏障重置为其初始状态 |
二、使用
下面我们通过一个简单的例子说明CyclicBarrier如何使用。
示例:模拟我们去聚会吃饭时要等所有人都都齐了才开始吃饭
public class T03_CyclicBarrier {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
for (int i = 1; i <= 10; i++) {
System.out.println("A" + i + "开始等待其他人...");
new Thread(new Person(cyclicBarrier), "A" + i).start();
}
}
}
class Person implements Runnable {
/**
* 循环栅栏
*/
private CyclicBarrier cyclicBarrier;
public Person(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在等待其他人到来....");
//线程阻塞
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
//模拟吃饭操作
System.out.println(Thread.currentThread().getName() + "开始吃饭....");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "吃完饭了....");
}
}
运行结果:
A1开始等待其他人...
A2开始等待其他人...
A3开始等待其他人...
A4开始等待其他人...
A5开始等待其他人...
A6开始等待其他人...
A7开始等待其他人...
A8开始等待其他人...
A9开始等待其他人...
A10开始等待其他人...
A1正在等待其他人到来....
A3正在等待其他人到来....
A5正在等待其他人到来....
A4正在等待其他人到来....
A2正在等待其他人到来....
A6正在等待其他人到来....
A7正在等待其他人到来....
A8正在等待其他人到来....
A9正在等待其他人到来....
A10正在等待其他人到来....
A10开始吃饭....
A1开始吃饭....
A2开始吃饭....
A4开始吃饭....
A6开始吃饭....
A7开始吃饭....
A5开始吃饭....
A3开始吃饭....
A8开始吃饭....
A9开始吃饭....
A8吃完饭了....
A1吃完饭了....
A3吃完饭了....
A9吃完饭了....
A10吃完饭了....
A6吃完饭了....
A2吃完饭了....
A7吃完饭了....
A5吃完饭了....
A4吃完饭了....
由执行结果看,只有10个人都到来,所有人即10个线程才开始执行“开始吃饭”操作。
下面我们测试一下使用第二种方式创建CyclicBarrier:
CyclicBarrier cyclicBarrier = new CyclicBarrier(10,()->{
System.out.println("===========所有人都完成等待,准备吃饭===========");
});
运行结果:
A1开始等待其他人...
A2开始等待其他人...
A3开始等待其他人...
A4开始等待其他人...
A5开始等待其他人...
A6开始等待其他人...
A7开始等待其他人...
A1正在等待其他人到来....
A8开始等待其他人...
A9开始等待其他人...
A10开始等待其他人...
A2正在等待其他人到来....
A6正在等待其他人到来....
A7正在等待其他人到来....
A3正在等待其他人到来....
A4正在等待其他人到来....
A5正在等待其他人到来....
A8正在等待其他人到来....
A9正在等待其他人到来....
A10正在等待其他人到来....
===========所有人都完成等待,准备吃饭===========
A10开始吃饭....
A1开始吃饭....
A2开始吃饭....
A6开始吃饭....
A7开始吃饭....
A3开始吃饭....
A4开始吃饭....
A5开始吃饭....
A8开始吃饭....
A9开始吃饭....
A8吃完饭了....
A1吃完饭了....
A2吃完饭了....
A7吃完饭了....
A3吃完饭了....
A6吃完饭了....
A4吃完饭了....
A9吃完饭了....
A5吃完饭了....
A10吃完饭了....
可以看到,那个Runnable barrierAction感觉就像是一个回调方法,等所有线程都到达那个屏障后,各个线程还没开始执行业务时,进行一些操作。
三、源码解读
CyclicBarrier内部是基于ReentrantLock可重入锁和Condition来实现的,这两个后面也会介绍。
CyclicBarrier类主要提供了两个构造方法:
- CyclicBarrier(int parties):parties表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
- CyclicBarrier(int parties, Runnable barrierAction):当所有线程到达屏障时,优先执行barrierAction,可以进行一些额外的操作。
CyclicBarrier内部存在一个私有的静态内部类Generation:用来描述CyclicBarrier的更新换代。在CyclicBarrier中,同一批的线程属于同一代。当所有线程都到达栅栏之后,Generation 则会进行更新换代。
private static class Generation {
//标识该当前CyclicBarrier是否已经处于中断状态
//默认为非中断状态
boolean broken = false;
}
属性说明:
//使用可重入锁ReentrantLock 来保护栅栏
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;
下面我们看一下其中比较重要的方法 ------- await()方法
- await()方法主要是告诉CyclicBarrier我已经到达了栅栏点,并且会阻塞当前线程,直到所有线程都到达栅栏点。
//不带超时时间的await方法
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
//带超时时间的await方法
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
上面两个方法都是调用的dowait()方法:主要屏障代码,涵盖各种政策。
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
//可重入锁
final ReentrantLock lock = this.lock;
//获取锁对象
lock.lock();
try {
//当前代
final Generation g = generation;
//如果当前代broken标志位true,表示CyclicBarrier已损坏,抛出BrokenBarrierException异常
if (g.broken)
throw new BrokenBarrierException();
//如果当前线程被中断,抛出InterruptedException中断异常
if (Thread.interrupted()) {
//将当前的屏障生成设置为被打破,并唤醒所有人。只有在持有锁时才调用。
breakBarrier();
throw new InterruptedException();
}
//获取下标索引
int index = --count;
//如果index为0,说明所有线程都到达了栅栏处
if (index == 0) { // tripped
//栅栏任务执行成功与否标志
boolean ranAction = false;
try {
//执行栅栏任务
final Runnable command = barrierCommand;
if (command != null)
//调用run()方法执行具体的任务
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) {
//将当前的屏障生成设置为被打破,并唤醒所有人
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();
}
}
//如果当前代broken标志位true,表示CyclicBarrier已损坏,抛出BrokenBarrierException异常
if (g.broken)
throw new BrokenBarrierException();
//如果正常换代时执行了nextGeneration();后generation重新生成了下一代
//如果不相等的话说明是正常更新换代,返回当前下标索引
if (g != generation)
return index;
//如果超时了或者指定的限制时间<=0,那么将当前的屏障生成设置为被打破,并唤醒所有人
if (timed && nanos <= 0L) {
breakBarrier();
//抛出超时异常
throw new TimeoutException();
}
}
} finally {
//手动释放锁对象
lock.unlock();
}
}
可以看到,如果该线程不是最后一个调用await方法的线程,则它会一直处于等待状态,除非发生以下情况:
- 最后一个线程到达,即index == 0;
- 某个参与线程等待超时;
- 某个参与线程被中断;
- 调用了CyclicBarrier的reset()方法,该方法会将屏障重置为初始状态;
- 更新换代的方法nextGeneration():触发时机就是当所有线程都已经到达栅栏处(即index == 0)
//更新栅栏的状态并且唤醒所有其他等待的线程
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// 建立下一代,重新初始化计数器的值
count = parties;
//重新生成Generation对象
generation = new Generation();
}
- 销毁CyclicBarrier的方法breakBarrier():
private void breakBarrier() {
//将标志修改为true
generation.broken = true;
// 建立下一代,重新初始化计数器的值
count = parties;
//唤醒所有其他等待的线程
trip.signalAll();
}
- 其他的一些方法说明:
//返回需要跨越此屏障的参与方的数量。
public int getParties() {
return parties;
}
//查询此屏障是否处于中断状态。
public boolean isBroken() {
//通过独占锁来实现,防止其他线程修改broken标志
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
//重置CyclicBarrier的状态
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
//返回当前在屏障处等待的参与方的数量
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}
四、总结
下面将CyclicBarrier和CountDownLatch进行一下比较,区别如下:
- CountDownLatch的计数器只能使用一次,而CyclicBarrier可以重复使用,所以CyclicBarrier能够处理更为复杂的场景;
- CyclicBarrier还提供了一些其他有用的API,在上面源码的时候已经介绍。比如getNumberWaiting()、isBroken()方法等待;
- CountDownLatch用于一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置;
来源:CSDN
作者:人丑就该多读书呀
链接:https://blog.csdn.net/Weixiaohuai/article/details/104568945