Android 并发编程—CyclicBarrier

1.CyclicBarrier循环屏障
CyclicBarrier是一个可循环使用的屏障锁,依赖于ReentrantLock和Cindition实现线程的阻塞和唤醒。
CyclicBarrier可以使一定数量的线程反复在屏障位置处汇集,当线程调用await方法时表示到达屏障位置,这个方法将使线程阻塞直到所有线程都到达屏障位置。当所有线程都到达屏障位置时屏障将打开,此时所有被屏障拦截的线程都将被释放,而屏障将被重置以便下次使用。
CyclicBarrier使用构造方法初始化指定parties(屏障数,即总线程数)和count(剩余屏障数,即还有多少线程未到达屏障),只需要使用await()方法即可让count值持续递减(count从parties到0递减,每换一代generation就减1)。基于Condition条件,屏障锁对于屏障数分为多组,每组中使用ReentrantLock产生的condition条件进行阻塞,中途发生中断或异常会直接唤醒当组被阻塞的线程并重置下一组的初始条件。

CyclicBarrier和CountDownLatch很像,它们最主要的区别是:CyclicBarrier的等待是可以循环的,等待完之后还可以重新使用。

使用场景:
①多个线程在执行过程中需要达到某一条件后再统一执行剩下的各自线程不一样的逻辑场景,如预初始化辅助类和共同逻辑业务数据计算。
(2)多个不同的线程任务,将自身负责的加载配置完成后等待打破屏障后,再执行剩余的各自逻辑或统一逻辑,如汇总后的日志。
注意:为了程序的健壮性,尽量给出合适的时间,防止多组线程数未达到屏障数导致无法唤醒,如最后一组线程数不够屏障数会导致程序无法结束。

CyclicBarrier特点:
①子线程调用await()方法会阻塞自身,不会阻塞主线程;
②可循环使用该屏障,每组都基于ReentrantLock产生的Condition的锁来阻塞自身,等到该组线程都执行了则对该组进行全部唤醒;
③当子线程发生中断抛出异常,会直接唤醒当组被阻塞的所有线程并重置下一组的初始条件,不会导致程序异常。

2.CyclicBarrier使用
public class Statistic implements Runnable {
private CyclicBarrier mCyclicBarrier;

public Statistic(CyclicBarrier parties) {
this.mCyclicBarrier = parties;
}

@Override
public void run() {
Log.e(TAG, "mCyclicBarrier: " + Thread.currentThread().getName() + “开始” + mCyclicBarrier.getNumberWaiting());
try {
long duration = (long) (Math.random() * 20);
TimeUnit.SECONDS.sleep(duration);
mCyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(“tag”, "mCyclicBarrier: " + Thread.currentThread().getName() + “结束”);
}
}

创建3个Statistic线程类:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
for (int i = 0; i < 3; i++) {
new Thread(new Statistic(cyclicBarrier)).start();
}
}
}

查看日志:
mCyclicBarrier: Thread-143开始0
mCyclicBarrier: Thread-144开始0
mCyclicBarrier: Thread-142开始0
mCyclicBarrier: Thread-143结束
mCyclicBarrier: Thread-144结束
mCyclicBarrier: Thread-142结束

每个线程执行完操作之后,就在等待其他线程的操作完毕,然后一起执行,即当所有线程写入操作完毕之后,所有线程就继续进行后续的操作了。

如果想要在所有线程执行完之后,进行额外的其他操作,可以为CyclicBarrier构造函数提供Runnable参数:
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
Log.e(“tag”, “执行完,做额外的操作,然后接着后续的操作。”);
}
});
for (int i = 0; i < 3; i++) {
new Thread(new Statistic(cyclicBarrier)).start();
}

查看日志:
mCyclicBarrier: Thread-154开始0
mCyclicBarrier: Thread-152开始0
mCyclicBarrier: Thread-153开始0
执行完,做额外的操作,然后接着后续的操作。
mCyclicBarrier: Thread-152结束
mCyclicBarrier: Thread-153结束
mCyclicBarrier: Thread-154结束

当3个线程都到达barrier状态后,会从3个线程中选择最后一个到达同步点的线程执行Runnable,然后继续后续操作。
注意:CyclicBarrier可以复用,而CountDownLatch无法进行重复使用。

3.CyclicBarrier原理
CyclicBarrier是基于ReentrantLock可重入锁和Condition来实现的。

①CyclicBarrier属性
private final int parties; 线程总数,表示需要统一行动的线程个数,每个线程调用await()告诉CyclicBarrier我已经到达屏障了,然后该线程被阻塞。
private int count; 计数器,即当前等待状态的线程。当一个线程到达屏障时count值就减1,当count减到0时,阻塞线程会被唤醒。
private final Runnable barrierCommand; 所有的线程到达屏障时,优先执行的动作,方便处理更复杂的业务场景。
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition(); //线程拦截器

②CyclicBarrier构造方法
public CyclicBarrier(int parties) {
this(parties, null);
}
创建一个CyclicBarrier,指定parties,当给定数量的线程等待时它将跳闸。

public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0)
throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
创建一个CyclicBarrier,并指定parties和barrierCommand,当给定数量的线程等待时它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。

③阻塞方法await()
CyclicBarrier的核心方法是await(),有两种方式,一个带时间参数,一个不带时间参数。await()方法本质上调用了ReentrantLock的newCondition().await()方法。
public int await() throws InterruptedException, BrokenBarrierException {
return dowait(false, 0L);
}
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
线程调用await()表示自己已经到达了屏障。
BrokenBarrierException表示屏障已经被破坏,破坏的原因可能是其中一个线程等待时被中断或者超时。

不管是定时等待还是非定时等待,最终都调用了dowait()方法。
dowait()方法主要是为CyclicBarrier普通等待和超时等待await服务 ,timed代表区分普通等待(false)和超时等待(true)、nanos代表超时时间。
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); //获取ReentrantLock锁;
try {
final Generation g = generation;
if (g.broken) //检查屏障是否被破坏
throw new BrokenBarrierException();
if (Thread.interrupted()) { //如果当前线程被中断,就破坏当前屏障并唤醒所有线程
breakBarrier();
throw new InterruptedException();
}
//每次调用await()方法count就减1,如果此时count为0则意味着所有线程都到达屏障,就唤醒所有等待的线程,并转换到下一代,即重置屏障
int index = --count;
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();
}
}
//如果计数器不为0,则进入循环,该线程进入等待状态,直到所有线程都到达屏障或当前线程被中断或超时这三者之一发生,当前线程才能继续执行
for ( ; ; ) {
try {
//执行condition.await()方法,把当前线程丢到条件队列。如果不是超时等待就调用await()进行等待;否则调用awaitNanos()等待
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) { //当前线程在等待过程中被中断,则破坏屏障唤醒所有线程
if (g ==generation && ! g.broken) {
breakBarrier();
throw ie;
} else { //若在捕获中断异常前已经完成在屏障上的等待,也直接调用中断操作
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();// 释放“独占锁(lock)”
}
}
CyclicBarrier中使用了ReentrantLock独享锁(可重入锁),当前线程调用await()方法其实是调用ReentrantLock的await(),进入条件等待队列里并释放锁;当所有的线程都到达屏障点后会选择最后一个到达屏障点的线程执行barrierCommand动作,然后调用ReentrantLock.signalAll()方法唤醒所有线程,线程将进入同步队列去竞争锁ReentrantLock (非公平锁),并调用nextGeneration()重置Generation,所以正常情况下重用CyclicBarrier可以不用调用reset方法,出现异常情况就需要调用reset方法重置状态。

await()方法中使用到了两个方法:
private void nextGeneration() {
trip.signalAll();
count = parties;
generation = new Generation();
}
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
count减为0或者线程在等待过程中被中断时,都调用condition.signalAll()方法把条件队列中的所有线程节点都移动到等待队列。
最后唤醒同步队列中的线程节点,线程从condition.await阻塞处醒来继续执行:获取ReentrantLock锁,用当前线程节点替换旧的头节点,最终释放ReentrantLock锁,继续让线程往下执行(每个线程依次获取、锁释放锁)。

Generation是CyclicBarrier的静态内部类,代表屏障的当前带,利用它可以实现循环等待:
private Generation generation = new Generation();
private static class Generation {
boolean broken = false;
}
public int getParties() {
return parties;
}
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try{
return generation.broken;
} finally {
lock.unlock();
}
}
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier();
nextGeneration();
} finally {
lock.unlock();
}
}
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}

CyclicBarrier原理总结:
假设线程thread1和线程thread2都执行到CyclicBarrier的await(),都进入dowait(boolean timed, long nanos)方法,由于dowait()方法有lock.lock()独享锁,假设thread1先获取到了独占锁,当它执行到–count时,index等于1,所以进入下面的for循环,接着执行trip.await(),进入condition.await()方法,在该方法里执行Node node = addConditionWaiter()将当前线程构造成Node节点并加入到Condition等待队列中,然后释放获取到的独占锁,当前线程进入阻塞状态;此时线程thread2就可以获取独占锁,继续执行–count,index等于0,所以先执行command.run()方法,然后执行nextGeneration()换代,nextGeneration()中trip.signalAll()只是将Condition等待队列中的Node节点按之前顺序都转移到AQS同步队列中,这里也就是将thread1对应的Node节点转移到了AQS同步队列中,thread2执行完nextGeneration(),返回return 0之前,细看代码还需要执行lock.unlock(),这里会执行到ReentrantLock的unlock()方法,最终执行到AQS的unparkSuccessor(Node node)方法,从AQS同步队列中的头结点开始释放节点,唤醒节点对应的线程,即thread1恢复执行。
如果有三个线程thread1、thread2和thread3,假设线程执行顺序是thread1、thread2、thread3,那么thread1、thread2对应的Node节点会被加入到Condition等待队列中,当thread3执行的时候会将thread1、thread2对应的Node节点按thread1、thread2顺序转移到AQS同步队列中,thread3执行lock.unlock()时会先唤醒thread1,thread1恢复继续执行,thread1执行到lock.unlock()的时候会唤醒thread2恢复执行。

4.CyclicBarrier和CountDownLatch的区别
①作用不同:CyclicBarrier要等固定数量的线程都到达了屏障位置后才能继续执行,而CountDownLatch只需要等待数字到0,也就是说CountDownLatch用于事件,CyclicBarrier是用于线程的。
②可重用性不同:CountDownLatch在倒数到0并触发门闩打开后就不能再次使用了,除非新建实例;而CyclicBarrier计数达到初始值,可以重置,重复使用。
③CountDownLatch是减计数方式,而CyclicBarrier是加计数方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值