聊聊并发:(十二)concurrent包并发辅助类之CyclicBarrier源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wtopps/article/details/84436498

前言

在前几篇文章中,

聊聊并发:(九)concurrent包之ReentrantLock分析

聊聊并发:(十一)concurrent包之Condition源码分析

我们对concurrent包中的locks下的几种锁的源码实现进行了分析,了解了它们的实现原理,在开发高并发的程序中,深入理解锁的使用是非常有必要的,如果没有读过前几篇的朋友,欢迎阅读。
本篇,我们继续分析concurrent包中的CyclicBarrier,CyclicBarrier是并发程序中经常用到的辅助类,可以帮助我们更好的编写并发程序,我们来一起了解一下它的使用方式以及实现机制。

CyclicBarrier的实现机制是依赖于ReentrantLock于Condition实现的,如果您对这两个类不了解,建议先阅读之前的文章关于这两个类的介绍。

CyclicBarrier介绍

CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时CyclicBarrier很有用。它的功能与Thread中的join()非常的相似,不过它的功能会更加的强大。

我们举一个生活中的例子:
在奥运会百米赛跑中,假设一共有10个跑道,分别有十个运动员,必须等待十个运动员全部在跑道准备就位,裁判员才可以开枪,任何一个运动员没有准备好之前,其他运动员都需要等待,这个就是CyclicBarrier的作用。

CyclicBarrier构造方法如下:

CyclicBarrier(int parties) 
创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。
------------------------------------------------------------------------------
CyclicBarrier(int parties, Runnable barrierAction) 
创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
------------------------------------------------------------------------------

CyclicBarrier的方法列表如下:

int await() 
在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。 
------------------------------------------------------------------------------
int await(long timeout, TimeUnit unit) 
在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。 
------------------------------------------------------------------------------
int getNumberWaiting() 
返回当前在屏障处等待的参与者数目。 
------------------------------------------------------------------------------
int getParties() 
返回要求启动此 barrier 的参与者数目。 
------------------------------------------------------------------------------
boolean isBroken() 
查询此屏障是否处于损坏状态。
------------------------------------------------------------------------------
void reset() 
将屏障重置为其初始状态。 
------------------------------------------------------------------------------

CyclicBarrier的构造方法,支持传入一个整形参数,代表执行线程的个数,每当一个线程执行await()方法后,该数字会减一,直至到0前,其他线程会一直等待。

CyclicBarrier使用示例

我们先来看一下CyclicBarrier是如何使用的:

public class CyclicBarrierDemo {
    public static void main(String[] args) throws Exception {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println("当前线程:" + Thread.currentThread().getName() + ", 等待其他线程准备就绪");
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
        System.out.println("全部线程就绪,开始执行");
    }
}

输出结果:

当前线程:Thread-0, 等待其他线程准备就绪
当前线程:Thread-3, 等待其他线程准备就绪
当前线程:Thread-1, 等待其他线程准备就绪
当前线程:Thread-4, 等待其他线程准备就绪
当前线程:Thread-2, 等待其他线程准备就绪
全部线程就绪,开始执行

这个示例非常的简单,我们创建了一个CyclicBarrier,设置其大小为5,然后新建5个线程,在线程方法中,执行await()操作,每次一个线程执行await(),计数器就会减一,直到减到0之前,其他线程都会等待;

根据结果我们也可以看到,当全部5个线程都执行完毕之后,才输出了"全部线程就绪,开始执行"。

CyclicBarrier源码实现

CyclicBarrier的实现依赖于ReentrantLock与Condition,对于这两个类的功能,前面的文章中我们已经详细介绍过,我们先看一下CyclicBarrier的大体结构:

public class CyclicBarrier {
    private static class Generation {
        boolean broken = false;
    }
    
    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();
    
    /** The number of parties */
    private final int parties;
    
    /* The command to run when tripped */
    private final Runnable barrierCommand;
    
    /** The current generation */
    private Generation generation = new Generation();
    
    private int count;
    
    ....
}

上面就是CyclicBarrier中几个主要的变量,基本上大家通过注释可以看懂是干嘛的,其中需要提的是Generation这个内部类,它的作用是每一个barrier代表了一个Generation的实例,Generation类有一个属性broken,用来表示当前barrier是否被损坏,Generation的状态对于CyclicBarrier的控制是非常重要的,后面我们会提到。

构造方法:

public CyclicBarrier(int parties, Runnable barrierAction) {
    //线程数目小与等于0,抛出异常
    if (parties <= 0) {
        throw new IllegalArgumentException();   
    }
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

构造方法比较简单,可以指定关联该CyclicBarrier的线程数量,并且可以指定在所有线程都进入屏障后的执行动作,该执行动作由最后一个进行屏障的线程执行。

如果不指定Runnable对象,即不进行任何操作。

await()

await()是CyclicBarrier最主要的一个方法,其作用我们在上面已经提过了,现在我们看一下其源码实现:

public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

核心是调用了dowait()方法,我们继续看:

/**
 * Main barrier code, covering the various policies.
 */
private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
    //1、获取锁
    lock.lock();
    try {
        //2、保存当前代
        final Generation g = generation;

        if (g.broken)
            throw new BrokenBarrierException();

        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
        //3、计数器自减
        int index = --count;
        //4、当计数器为0时,结束流程
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                //5、获取结束时执行动作
                final Runnable command = barrierCommand;
                //如果动作不为空,执行
                if (command != null)
                    command.run();
                ranAction = true;
                //6、重置当前代
                nextGeneration();
                return 0;
            } finally {
                //7、未执行任何动作,破坏掉栅栏
                if (!ranAction)
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        // 进行死循环,直到被破坏、打断、超时,结束循环
        for (;;) {
            try {
                //8、如果未设置超时,当前线程进入Condition的等待队列
                if (!timed)
                    trip.await();
                //如果设置了超时时间,当前线程在超时时间之前,进入等待队列等待
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                //9、如果出现打断异常,判断保存的代等于当前代并且屏障没有被损坏
                if (g == generation && ! g.broken) {
                    //10、破坏掉栅栏
                    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();
                }
            }
            //11、如果保存的代被破坏,抛出异常
            if (g.broken)
                throw new BrokenBarrierException();
            //12、如果保存的代不等于当前代,返回index
            if (g != generation)
                return index;
            //13、如果设置了等待时间,并且等待时间小于0,破坏栅栏,并抛出异常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        //14、释放锁资源
        lock.unlock();
    }
}

上面就是dowait()方法的实现,具体流程请参见注释,整体流程并不复杂,其中依赖于ReentrantLock与Condition类,这两个类的用法与实现机制我们在前面的文章中介绍过,不了解的读者可以点击这里。

在dowait()方法中,调用了两个方法,分别是nextGeneration()与breakBarrier(),这两个方法非常的重要,我们来看一下其实现:

nextGeneration():

private void nextGeneration() {
    // signal completion of last generation
    trip.signalAll();
    // set up next generation
    count = parties;
    generation = new Generation();
}

nextGeneration()主要的作用是唤醒在当前Condition对象的等待队列中等待的全部线程,并更新当前代,代的概念这里我们说一下,它是CyclicBarrier中的一个内部类,它的作用更像是一个标志位的作用,其只有一个属性,broken,记录当前代释是否被破坏。

nextGeneration()会在全部线程进入屏障后会被调用,即生成下一个代,使得全部线程又可以重新进入到栅栏中,从这里可以得知,CyclicBarrier的栅栏是可以多次复用的,而这个特性与另一个功能相似的类CountDownLatch有所不同,后面的篇幅中我们会分析CountDownLatch。

我们再看一下另一个方法,breakBarrier():

private void breakBarrier() {
    generation.broken = true;
    count = parties;
    trip.signalAll();
}

breakBarrier()方法比较简单,将当前代的破坏状态设置为true,并唤醒在当前Condition对象的等待队列中等待的全部线程。

OK,dowait()方法的核心实现我们已经看完了,我们用一张图来描述下它的工作流程:

image

OK,await()方法我们就介绍在这么多,接下来我们看一下reset()方法的实现:

reset()

public void reset() {
    final ReentrantLock lock = this.lock;
    //1、获取锁
    lock.lock();
    try {
        // 2、破坏当前代
        breakBarrier();
        // 3、创建新的代
        nextGeneration();
    } finally {
        lock.unlock();
    }
}

reset()方法的实现如上,可以看到比较简单,首先获取锁,然后破坏掉当前代,并创建新的代。

复用性

前面我们提到过,CyclicBarrier是可以复用的,那么这个应该如何理解呢?我们用一个小Demo来演示一下:

public class CyclicBarrierDemo {
    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println("线程准备进入等待,当前线程:" + Thread.currentThread().getName());
                    cyclicBarrier.await();
                    System.out.println("全部线程就位,第一轮结束");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println("线程准备进入等待,当前线程:" + Thread.currentThread().getName());
                    cyclicBarrier.await();
                    System.out.println("全部线程就位,第二轮结束");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

输出结果:

线程准备进入等待,当前线程:Thread-1
线程准备进入等待,当前线程:Thread-0
线程准备进入等待,当前线程:Thread-2
线程准备进入等待,当前线程:Thread-3
线程准备进入等待,当前线程:Thread-4
全部线程就位,第一轮结束
全部线程就位,第一轮结束
全部线程就位,第一轮结束
全部线程就位,第一轮结束
全部线程就位,第一轮结束
线程准备进入等待,当前线程:Thread-5
线程准备进入等待,当前线程:Thread-6
线程准备进入等待,当前线程:Thread-9
线程准备进入等待,当前线程:Thread-7
线程准备进入等待,当前线程:Thread-8
全部线程就位,第二轮结束
全部线程就位,第二轮结束
全部线程就位,第二轮结束
全部线程就位,第二轮结束
全部线程就位,第二轮结束

在这个示例中(比较low),我们创建了一个CyclicBarrier,初始化大小设置为5,然后依次启动两组线程,每组线程大小为5,在线程中执行await(),从执行结果中我们可以看出,第一轮线程组执行完成后,在执行第二轮的时候,CyclicBarrier进行了重新初始化。

其机制我们在源码中已经提到过了,是因为当计数器为0的时候,会调用nextGeneration()方法,初始化下一个代,因此,CyclicBarrier是可以复用的。

使用场景

CyclicBarrier可以用于多个线程执行任务,需要等待多个线程全部执行完毕后,才可以输出最终结果,其在多线程开发场景下,非常的常用。

结语

本篇我们介绍了CyclicBarrier的用法与实现机制,读完后大家可能发现,CyclicBarrier的实现并不复杂,其核心主要依赖于ReentrantLock与Condition,如果您对这两个类的机制不是很了解的话,建议去看看前面的文章对于它们的介绍。

聊聊并发:(九)concurrent包之ReentrantLock分析

聊聊并发:(十一)concurrent包之Condition源码分析

感谢您的阅读!!!

下篇预告:

聊聊并发:(十三)concurrent包并发辅助类之CountDownLatch源码分析

敬请期待~

更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/wtopps/article/details/84436498