并发编程之CyclicBarrier详解

一、概述

CyclicBarrier(循环屏障),它是一个同步助手工具,它允许多个线程在执行完相应的操作之后彼此等待共同到达一个障点(barrier point)。CyclicBarrier 也非常适合用于某个串行化任务被分拆成若干个并行执行的子任务,当所有的子任务都执行结束之后再继续接下来的工作。

CyclicBarrier 使一定数量的线程反复地在屏障位置处汇集。当线程到达屏障位置时将调用 await 方法,这个方法将会阻塞,直到所有线程都到达屏障位置,当所有线程都到达屏障位置,那么屏障将打开,此时所有的线程都将被唤醒,而屏障将被重置以便下次使用。

二、源码解析

1、类结构图

通过  CyclicBarrier 代码结构我们可以发现其内部使用了 独占锁  ReentrantLock 和 Condition 来实现线程之间的等待通知功能。

2、构造方法

CyclicBarrier 一共提供了两个构造方法,分布如下:

  • CyclicBarrier(int parties):参数 parties  表示屏障拦截的线程数量,必须有 parties 个线程调用了 await() 方法后此屏障才会被打开。
  • CyclicBarrier(int parties, Runnable barrierAction):参数 parties  同上。参数 barrierAction 表示当所有线程都到达屏障点后,需要执行的任务。

注意:

  1. parties 参数必须 大于 0。否则会抛出异常。
  2. barrierAction 可以为null,  因为 barrierAction  中的代码是在最后一个到达屏障点的线程中执行,建议不要在此内部实现耗时较大的任务业务逻辑。

 3、await方法 

await 存在两个重载方法,两个代码的实现非常相似,内部都是又调用了私有的 dowait 方法,两者唯一区别就是,其中一个带有超时时间,如下所示:

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L); //调用 dowait 方法
        } 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)); // 调用 dowait 方法
    }

await() 方法的作用是告诉 CyclicBarrier 我已经到达屏障点。但是,如果当前调用 await() 方法的线程不是最后一个到达的,它将被休眠阻塞,直到以下其中一种情况发生,

  • 最后一个线程到达屏障到
  • 当前线程被中断
  • 正在屏障点等待的线程被中断
  • 在屏障点的线程等待超时
  • 有其他线程调用了 reset() 方法重置了屏障点

4、dowait() 方法

从上面源码我们可以知道,await 方法内部其实都是调用了 dowait() 方法,那么接下来重点分析一下 dowait 方法。

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();
 
        // 如果线程中断了,抛出异常
        if (Thread.interrupted()) {
            // 将损坏状态设置为true
            // 并通知其他阻塞在此栅栏上的线程
            breakBarrier();
            throw new InterruptedException();
        }
 
        // 获取下标
        int index = --count;
        // 如果是 0,说明最后一个线程调用了该方法
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                // 执行栅栏任务
                if (command != null)
                    command.run();
                ranAction = true;
                // 更新一代,将count重置,将generation重置
                // 唤醒之前等待的线程
                nextGeneration();
                return 0;
            } finally {
                // 如果执行栅栏任务的时候失败了,就将损坏状态设置为true
                if (!ranAction)
                    breakBarrier();
            }
        }
 
        // loop until tripped, broken, interrupted, or timed out
        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 {
                    // 上面条件不满足,说明这个线程不是这代的
                    // 就不会影响当前这代栅栏的执行,所以,就打个中断标记
                    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();
    }
}

dowait() 方法的代码逻辑也很简单,大致流程如下:

在上面的源代码中,我们可能需要注意Generation 对象,在上述代码中我们总是可以看到抛出BrokenBarrierException异常,那么什么时候抛出异常呢?如果一个线程处于等待状态时,如果其他线程调用reset(),或者调用的barrier原本就是被损坏的,则抛出BrokenBarrierException异常。同时,任何线程在等待时被中断了,则其他所有线程都将抛出BrokenBarrierException异常,并将barrier置于损坏状态。

同时,Generation描述着CyclicBarrier的更新换代。在CyclicBarrier中,同一批线程属于同一代。当有parties个线程到达barrier之后,generation就会被更新换代。其中broken标识该当前CyclicBarrier是否已经处于中断状态。

    private static class Generation {
        boolean broken = false;
    }

默认barrier是没有损坏的。当barrier损坏了或者有一个线程中断了,则通过breakBarrier()来终止所有的线程:

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

在breakBarrier()中除了将broken设置为true,还会调用signalAll将在CyclicBarrier处于等待状态的线程全部唤醒。

当所有线程都已经到达barrier处(index == 0),则会通过nextGeneration()进行更新换地操作,在这个步骤中,做了三件事:唤醒所有线程,重置count,generation:

    private void nextGeneration() {
        // 唤醒 等待的线程
        trip.signalAll();
        // 重置计数器
        count = parties;
        // 更新年代
        generation = new Generation();
    }

除了上面讲到的栅栏更新换代以及损坏状态,我们在使用CyclicBarrier时还要要注意以下几点:

  • CyclicBarrier使用独占锁来执行await方法,并发性可能不是很高
  • 如果在等待过程中,线程被中断了,就抛出异常。但如果中断的线程所对应的CyclicBarrier不是这代的,比如,在最后一次线程执行signalAll后,并且更新了这个“代”对象。在这个区间,这个线程被中断了,那么,JDK认为任务已经完成了,就不必在乎中断了,只需要打个标记。该部分源码已在dowait(boolean, long)方法中进行了注释。
  • 如果线程被其他的CyclicBarrier唤醒了,那么g肯定等于generation,这个事件就不能return了,而是继续循环阻塞。反之,如果是当前CyclicBarrier唤醒的,就返回线程在CyclicBarrier的下标。完成了一次冲过栅栏的过程。该部分源码已在dowait(boolean, long)方法中进行了注释。

三、CyclicBarrier VS. CountDownLatch

  1.  CoundDownLatch的await方法会等待计数器被count down到0,而执行CyclicBarrier的await方法的线程将会等待其他线程到达barrier point。
  2.  CyclicBarrier内部的计数器count是可被重置的,进而使得CyclicBarrier也可被重复使用,而CoundDownLatch则不能。
  3.  CyclicBarrier是由Lock和Condition实现的,而CountDownLatch则是由同步控制器AQS(AbstractQueuedSynchronizer)来实现的。
  4.  在构造CyclicBarrier时不允许parties为0,而CountDownLatch则允许count为0。

四、实例

下面我们看一个简单的例子,想必每个人都是非常喜欢旅游的,旅游的时候不可避免地需要加入某些旅行团。在每一个旅行团中都至少会有一个导游为我们进行向导和解说,由于游客比较多,为了安全考虑导游经常会清点人数以防止个别旅客由于自由活动出现迷路、掉队的情况。

我们可以看到,只有在所有的旅客都上了大巴之后司机才能将车开到下一个旅游景点。下面写一个程序简单模拟一下。

public class CyclicBarrierExample {
    // 游客
    static class Tourist extends Thread{
        private final int touristId;
        private final CyclicBarrier barrier;

        public Tourist(int touristId, CyclicBarrier barrier){
              this.touristId = touristId;
              this.barrier = barrier;
        }

        @Override
        public void run() {
            System.out.printf("游客: %d 正在奔赴客车路上. \n", touristId);
             sleep(); //模拟游客等待
            System.out.printf("游客: %d 已上车, 等待其他游客 \n", touristId);
            try {
                barrier.await(); // 加入屏障等待其他游客
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
        public void sleep(){
            try {
                TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {

        CyclicBarrier barrier = new CyclicBarrier(11);

        IntStream.range(0, 10).forEach(i -> {
            new Tourist(i, barrier).start();
        });

        try {
            barrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }

        System.out.println("所有的游客都已上车, 开始发车去往景点");

    }
}

猜你喜欢

转载自blog.csdn.net/small_love/article/details/111226098