CyclicBarrier — 多线程中的循环栅栏

1. CyclicBarrier简介

现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才开始。例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。

在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类。利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点(栅栏)后再进行后续的操作。下图演示了这一过程:

CyclicBarrier可以使一定数量的线程反复地在栅栏(不同轮次或不同代)位置处汇集。

CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 和 CountDownLatch 很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。

当线程到达栅栏位置时将调用await()方法,这个方法将阻塞(当前线程)直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

2.CyclicBarrier的使用

2.1 常用方法

//参数parties:表示要到达屏障 (栅栏)的线程数量
//参数Runnable: 最后一个线程到达屏障之后要做的任务
​
//构造方法1
public CyclicBarrier(int parties) 
//构造方法2
public CyclicBarrier(int parties, Runnable barrierAction)
​
//线程调用await()方法表示当前线程已经到达栅栏,然后会被阻塞
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
​
//带时限的阻塞等待
public int await(long timeout, TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException {
    return dowait(true, unit.toNanos(timeout));
}
 

2.2 使用举例

 适用场景:可用于需要多个线程均到达某一步之后才能继续往下执行的场景

//循环栅栏-可多次循环使用
   CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{
        System.out.println(Thread.currentThread().getName()+" 完成最后任务!");
   });
​
   IntStream.range(1,6).forEach(i->{
        new Thread(()->{
            try {
                Thread.sleep(new Double(Math.random()*3000).longValue());
                System.out.println(Thread.currentThread().getName()+" 到达栅栏A");
                
                cyclicBarrier.await();//屏障点A,当前线程会阻塞至此,等待计数器=0
                
                System.out.println(Thread.currentThread().getName()+" 冲破栅栏A");
            }catch (Exception e){
                e.printStackTrace();
            }
        }).start();
    });

3.CyclicBarrier原理

CyclicBarrier是一道屏障,调用await()方法后,当前线程进入阻塞,当parties数量的线程调用await方法后,所有的await方法会返回并继续往下执行。

3.1 成员变量

 /** CyclicBarrier使用的排他锁*/
    private final ReentrantLock lock = new ReentrantLock();
    /** barrier被冲破前,线程等待的condition*/
    private final Condition trip = lock.newCondition();
    /** barrier被冲破时,需要满足的参与线程数。*/
    private final int parties;
    /* barrier被冲破后执行的方法。*/
    private final Runnable barrierCommand;
    /** 当其轮次 */
    private Generation generation = new Generation();
​
    /**
     *目前等待剩余的参与者数量。从 parties倒数到0。每个轮次该值会被重置回parties
     */
    private int count;

(1)CyclicBarrier内部是通过条件队列trip来对线程进行阻塞的,并且其内部维护了两个int型的变量parties和count。

  • parties表示每次拦截的线程数,该值在构造时进行赋值。

  • count是内部计数器,它的初始值和parties相同,以后随着每次await方法的调用而减1,直到减为0就将所有线程唤醒。

(2)CyclicBarrier有一个静态内部类Generation,该类的对象代表栅栏的当前代,就像玩游戏时代表的本局游戏,利用它可以实现循环等待

(3)barrierCommand表示换代前执行的任务,当count减为0时表示本局游戏结束,需要转到下一局。在转到下一局游戏之前会将所有阻塞的线程唤醒,在唤醒所有线程之前你可以通过指定barrierCommand来执行自己的任务。

3.2 构造器

//构造器1:指定本局要拦截的线程数parties 及 本局结束时要执行的任务
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}
​
//构造器2
public CyclicBarrier(int parties) {
    this(parties, null);
}

3.3 等待的方法

CyclicBarrier类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的方法,分别是定时等待非定时等待。源代码中await()方法最终调用的是dowait()方法:

private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException,TimeoutException {
    // 获取独占锁
    final ReentrantLock lock = this.lock;
    lock.lock();//对共享资源count,generation操作前,需先上锁保证线程安全
    try {
        // 当前代--当前轮次对象的引用
        final Generation g = generation;
        // 如果这轮次broken了,抛出异常
        if (g.broken)
            throw new BrokenBarrierException();
        // 如果线程中断了,抛出异常
        if (Thread.interrupted()) {
            breakBarrier();//如果被打断,通过此方法设置当前轮次为broken状态,通知当前轮次所有等待的线程
            throw new InterruptedException();//抛出异常
        }
 
        //自旋前
        
        //1、count值-1
        int index = --count;
        // 2、判断是否到0,若是,则冲破屏障点(说明最后一个线程已经到达)
        if (index == 0) {
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                // 3、执行栅栏任务(若CyclicBarrier构造时传入了Runnable,则调用)
                if (command != null)
                    command.run();
                ranAction = true;
                // 4、更新一轮次,将count重置,将generation重置,唤醒之前等待的线程
                nextGeneration();
                return 0;
            } finally {
                // 如果执行栅栏任务(command)的时候出现了异常,那么就认为本轮次破环了
                if (!ranAction)
                    breakBarrier();
            }
        }
 
        //计数器没有到0 =》开始自旋,直到屏障被冲破,或者interrupted或者超时
        for (;;) {
            try {
                 // 开始等待;如果没有时间限制,则直接等待,直到被唤醒(让其他线程进入到lock的代码块执行以上逻辑)
                if (!timed)
                    trip.await();//阻塞,此时会释放锁,以让其他线程进入await方法中,等待屏障被冲破后,向后执行
                // 如果有时间限制,则等待指定时间
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                // 如果当前线程阻塞被interrupt了,并且本轮次还没有被break,那么修改本轮次状态为broken
                if (g == generation && ! g.broken) {
                    // 让栅栏失效
                    breakBarrier();
                    throw ie;
                } else {
                    // 上面条件不满足,说明这个线程不是这轮次的,就不会影响当前这代栅栏的执行,所以,就打个中断标记
                    Thread.currentThread().interrupt();
                }
            }
 
            // 当有任何一个线程中断了,就会调用breakBarrier方法,就会唤醒其他的线程,其他线程醒来后,也要抛出异常 
            if (g.broken)
                throw new BrokenBarrierException();
 
            // g != generation表示正常换轮次了,返回当前线程所在栅栏的下标
            // 如果 g == generation,说明还没有换,那为什么会醒了?
            // 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。
            // 正是因为这个原因,才需要generation来保证正确。
            if (g != generation)
                return index;
            
            // 如果有时间限制,且时间小于等于0,销毁栅栏并抛出异常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }//自旋
        
    } finally {
        // 释放独占锁
        lock.unlock();
    }
}

更新本轮次的方法:nextGeneration()

private void nextGeneration() {
    trip.signalAll();//唤醒本轮次等待的线程
    count = parties;//重置count值为初始值,为下一轮次(代)使用
    generation = new Generation();//更新本轮次对象,这样自旋中的线程才会跳出自旋。
}
​
private static class Generation {
    boolean broken = false;
}
​
private void breakBarrier() {
    generation.broken = true;//设置标识
    count = parties;//重置count值为初始值
    trip.signalAll();//唤醒所有等待线程
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值