java栅栏--CyclicBarrier

CyclicBarrier介绍

 

java栅栏--CyclicBarrier,直接翻译为循环屏障。其作用与前面讲的闭锁CountDownLatch有些类似,可以构造一个可循环利用的“屏障”,在一组线程执行任务到达指定位置之前 阻塞已经到达的线程,等所有线程都到达指定位置后,同时依次唤醒所有线程。

 

CountDownLatch也可以阻塞一组线程,但在使用上与CyclicBarrier有所区别。CountDownLatch可以实现阻塞一组线程等待另一组线程执行完成,这里可以有两组线程;而CyclicBarrier只是阻塞一组线程,另外它还可以重复使用,并且还允许所有线程到达指定位置后 先执行一段方法,再依次唤醒这组线程继续继续,这些都是CountDownLatch所不具备的。

 

下面还是以讲解CountDownLatch时,使用的多人准备游戏为例,简单暂时下CyclicBarrier的用法:

public class CyclicBarrierTest {
    public static void main(String[] args) {
        ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
 
        CyclicBarrier playersCounter = new CyclicBarrier(10, new Runnable() {
            @Override
            public void run() {
                System.out.println("所有玩家准备就绪,开始游戏!");
            }
        });
        System.out.println("等待玩家加入游戏");
 
        for (int i=0;i<20;i++){
            executorService.execute(new Player(playersCounter,i+""));
        }
 
        playersCounter.isBroken();
 
    }
}
 
 
//多线程操作线程不安全容器 ThreadSafe.datas
class Player implements Runnable{
    private CyclicBarrier playersCounter;
    private String name;
 
    public Player(CyclicBarrier playersCounter,String name) {
        this.playersCounter = playersCounter;
        this.name = name;
    }
    @Override
    public void run() {
        System.out.println("玩家:"+name+"加入游戏");
        try {
            Thread.sleep(1000);
            playersCounter.await();
            System.out.println("玩家:"+name+"开始选英雄");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意与CountDownLatch版本的实现有三点区别:

1CountDownLatch是在主线程调用await阻塞(实际上可以在一组线程上await),而CyclicBarrier是在任务线程里await阻塞。

2CountDownLatch只能使用一次,也就是新建一个CountDownLatch对象对应10个玩家准备游戏,另外10个玩家准备时 又得创建一个新的CountDownLatch对象;而CyclicBarrier可以重复使用,这里可以看到20个玩家,分别被分成了两组开始游戏。

3、每10个玩家准备就绪后,在CyclicBarrier实现中可以先执行一段Runnablerun方法,再依次唤醒线程,这种处理方式在阶段性为题中非常有用。

 

在简单了解CyclicBarrier的使用方法之后,继续开始深入理解CyclicBarrier的实现原理。

 

CyclicBarrier实现原理

 

前面讲过CountDownLatch是基于AQS实现的;而CyclicBarrier是基于ReentrantLock重入锁实现的,当然ReentrantLock也是基于AQS实现的,非要说CyclicBarrier也是基于AQS实现的也不为过。

 

重要成员变量

CyclicBarrier定义了下列几个成员变量,其核心方法都会操作这几个成员变量。这里先列出来,结合具体方法使用到时 方便查看和理解。

 

    //可以理解为初始化时 需要阻塞的任务个数
    private final int parties;
    //剩余需要等待的任务个数,初始值为parties,直到为0时依次唤醒所有被阻塞的任务线程。
    private int count;
 
    //每次对“栅栏”的主要成员变量进行变更操作,都应该加锁
    private final ReentrantLock lock = new ReentrantLock();
    //用于阻塞和唤醒任务线程
   private final Condition trip = lock.newCondition();
 
    //在所有线程被唤醒前,需要执行的一个Runable对应的run方法
    private final Runnable barrierCommand;
    //用于表示“栅栏”当前的状态
    private Generation generation = new Generation();
 

 

其中只有countgeneration不是final的,也就是说其他几个成员变量初始化后是不允许修改的。

 

构造方法

CyclicBarrier有两个重载的构造方法,一个是不带Runnable参数,另一个带有Runnable参数。本质上都会调用带Runnable参数的构造方法进行实例化,这里只贴出带Runnable参数的构造方法实现:

public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties; //为了实现复用,进行备份
        this.count = parties;//初始化,待阻塞的任务总数
        this.barrierCommand = barrierAction;//初始化
    }

 

构造方法很好理解,主要工作就是初始化三个重要的成员变量。

 

await方法

await方法是CyclicBarrier的核心方法,本质上调用的是dowait,一组线程的阻塞和唤醒工作都在这个方法中实现:

public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
 
private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock(); //加锁
        try {
            final Generation g = generation;
 
            if (g.broken)
                throw new BrokenBarrierException();
            //有一个线程线程被中断,整个CyclicBarrier将不可用
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
 
            int index = --count; //待等待的任务数减1
            if (index == 0) {  // 如果待等待的任务数减至0,依次唤醒所有线程
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();//唤醒前先执行Runnable对象的run方法
                    ranAction = true;
                    nextGeneration();//重置整个CyclicBarrier,方便下次重用
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }
 
            //如果待等待的任务数大于0,进行线程阻塞,直到count为0时被唤醒
            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)//正常被唤醒,generation会被新建
                    return index;
 
                if (timed && nanos <= 0L) {//延迟阻塞时间到后唤醒
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
}
 

 

理解这个方法主要是理解两部分代码块:在count还没有到0之前,都会执行for循环着块代码,对线程进行阻塞;直到count变为0,执行if (index == 0)所在的代码块依次唤醒所有阻塞的线程,并对CyclicBarrier对象进行重置,方便继续使用。结合给出的注释,对await方法的实现应该就不难理解了。

 

另外await方法还有一个延时版本,主要是为了防止没有足够多线程调用await方法,count就不会减为0,线程就会被永久阻塞;如果使用延时版本的await方法就可以避免这个问题,时间到后,所有线程阻塞的线程都会被依次唤醒,并收到TimeoutException异常,避免被长时间阻塞。

 

isBroken方法

该方法主要用于判断当前CyclicBarrier对象的状态,如果状态为false,说明现在CyclicBarrier不可用,如果此时调用await方法 会直接收到一个BrokenBarrierException异常,见dowait方法。

 

reset()方法

该方法主要用于重置CyclicBarrier对象:

public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            breakBarrier();   //先把状态置为不可用,并唤醒被阻塞的线程执行完成任务
            nextGeneration(); //重置CyclicBarrier对象
        } finally {
            lock.unlock();
        }
    }
 

 

getNumberWaiting()方法

该方法用于获取当前已经被阻塞的任务数:

public int getNumberWaiting() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return parties - count; //计算已被阻塞的任务数
        } finally {
            lock.unlock();
        }
    }
 

 

到这里CyclicBarrier的实现原理分析结束。

 

CyclicBarrier的不足

 

CyclicBarrier相对于CountDownLatch来说,在分段执行任务上进行了改进,可以重复使用。并且在每个分段完成时,可以执行一个Runnable对应的run方法 做一些业务处理,比如:统计汇总。使用CyclicBarrier实现分阶段汇总:



 

假设把任务分为三个阶段,每个阶段结束的汇总方法相同,使用CyclicBarrier可以很简单的实现,这个“汇总方法”其实就是CyclicBarrier构造方法的第二参数,Runnable对应的run方法。 但如果每个阶段的汇总方法如果不一样时,CyclicBarrier就显得束手无措,因为它只能定义一个Runnable参数。如果遇到这种场景可以使用java1.7中新增的Phaser

 

Phaser的名字就可以看出,它主要就是用于解决分阶段问题,比起CyclicBarrier来,Phaser可以在每个阶段结束后执行不同的操作,从分阶段的角度看PhaserCyclicBarrier的增强版。从CountDownLatchCyclicBarrier 再到Phaser是一个依次增强的过程,但又不能相互完全取到。关于Phaser这里就不再深入讨论,后面有时间再单独总结。

 

猜你喜欢

转载自moon-walker.iteye.com/blog/2407828