并发编程系列(十二)—深入理解CyclicBarrier栅栏

并发编程系列(十二)—深入理解CyclicBarrier栅栏

前言

大家好,牧码心今天给大家推荐一篇并发编程系列(十二)—深入理解CyclicBarrier栅栏的文章,希望对你有所帮助。内容如下:

  • CyclicBarrier概要
  • CyclicBarrier数据结构
  • CyclicBarrier使用方式
  • CyclicBarrier实现原理

CyclicBarrier 概要

CyclicBarrier也是一个同步辅助器,功能和CountDownLatch有些类似。CountDownLatch是一个倒数计数器,在计数器不为0时,所有调用await的线程都会等待,当计数器降为0,线程才会继续执行,且计数器一旦变为0,就不能再重置了。
CyclicBarrier可以认为是一个可循环栅栏屏障,让一组线程达到栅栏屏障(也叫同步点)时被阻塞。直到最后一个线程达到屏障时。屏障才会打开,所有被屏障阻塞的线程才会继续运行。为了更好的理解,可以看下图所示:
如一共4个线程A、B、C、D,它们到达栅栏的顺序可能各不相同。当A、B、C到达栅栏后,由于没有满足总数【4】的要求,所以会一直等待,当线程D到达后,栅栏才会放行。
在这里插入图片描述
对比CountDownLatch有以下不同点:
1.CountDownlatch是允许1或N个线程等待其他线程完成执行。CyclicBarrier 是允许N个线程相互等待;
2. CountDownlatch的计数器无法被重置。CyclicBarrier 的可以被重置后循环使用,也叫循环的CyclicBarrier ;

CyclicBarrier 使用示例

模拟场景

5个运动员准备跑步比赛,运动员在赛跑前会准备一段时间,当裁判发现所有运动员准备完毕后,就举起发令枪,比赛开始。
这里的起跑线就是屏障,运动员必须在起跑线等待其他运动员准备完毕

public class CyclicBarrierTest {

    // 定义同时到达barrier的线程个数
    private static final int BARRIER_SIZE=5;
    // 定义barrier
    private static CyclicBarrier barrier;
    public static void main(String[] args) {
        barrier=new CyclicBarrier(BARRIER_SIZE, new Runnable() {
            @Override
            public void run() {
                System.out.println("=========所有运动员准备完毕,开始起跑!========");
            }
        });
        for(int i=0;i<5;i++){
            // 创建子线程
            new SubBarrierThread("运动员"+i).start();
        }
    }
    static class SubBarrierThread extends Thread{

        public SubBarrierThread(String name){
            super(name);
        }
        @Override
        public void run() {
            try {
                // 模拟运动员准备
                Thread.sleep(500);
                System.out.println(Thread.currentThread().getName() + " 已准备完毕!....");
                // 远动员准备好,barrier的数量+1
                barrier.await();
                // 达到barrier的数量为5时,才继续
                //System.out.println(Thread.currentThread().getName() + " continued.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

说明:从输出可以看到,线程到达栅栏时会被阻塞(调用await方法),直到到达栅栏的线程数满足指定数量要求时,栅栏才会打开放行。
细心的朋友会发现,若线程在阻塞过程中可能被中断,而CyclicBarrier放行的条件是等待的线程数达到指定数目,万一线程被中断导致最终的等待线程数达不到栅栏的要求怎么办?CyclicBarrier一定有考虑到这种异常情况,不然其它所有等待线程都会无限制地等待下去。
那么CyclicBarrier是如何处理的呢?我们看下CyclicBarrier的await()方法定义:

public int await() throws InterruptedException, BrokenBarrierException {
       // 业务逻辑
    }

可以看到await() 方法除了抛出InterruptedException异常外,还会抛出BrokenBarrierException。
BrokenBarrierException异常表示当前的CyclicBarrier已经损坏了,可能等不到所有线程都到达栅栏了,所以已经在等待的线程也没必要再等了,可以散伙了。
出现以下几种情况之一时,当前等待线程会抛出BrokenBarrierException异常:
1.其它某个正在await等待的线程被中断了
2.其它某个正在await等待的线程超时了
3.某个线程重置了CyclicBarrier(调用了reset方法,后面会讲到)
另外,只要正在Barrier上等待的任一线程抛出了异常,那么Barrier就会认为肯定是凑不齐所有线程了,就会将栅栏置为损坏(Broken)状态,并传播BrokenBarrierException给其它所有正在等待(await)的线程。

CyclicBarrier 数据结构

CyclicBarrier 依赖独占锁实现,我们看下其UML类图和主要函数。

  • UML类图
    CyclicBarrier  类图
    说明: CyclicBarrier是包含了独占锁ReentrantLock、Condition和记录每一轮的Generation,后面会分析如何实现依赖它们

  • 主要函数

// 初始化一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作
CyclicBarrier(int parties, Runnable barrierAction)
// 在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
int await()
// 在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。
int await(long timeout, TimeUnit unit)
// 返回要求启动此 barrier 的参与者数目。
int getParties()
// 查询此屏障是否处于损坏状态。
boolean isBroken()
// 重置屏障重置为其初始状态。
void reset()

CyclicBarrier 实现原理

CyclicBarrier 通过ReentrantLock(独占锁)和Condition来实现的。下面,我们分析CyclicBarrier中3个核心函数: 构造函数, await()作出分析

  • 初始化CyclicBarrier
    CyclicBarrier的构造函数共2个:CyclicBarrier 和 CyclicBarrier(int parties, Runnable barrierAction)。第1个构造函数是调用第2个构造函数来实现的,下面第2个构造函数的源码。
public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        // parties 表示必须同时到达栅栏点的线程数
        this.parties = parties;
        // count表示“处在等待状态的线程个数”。
        this.count = parties;
        // barrierCommand  表示栅栏点的线程个数都达到时,触发的动作;
        this.barrierCommand = barrierAction;
    }
  • CyclicBarrier的内部结构
public class CyclicBarrier {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition trip = lock.newCondition();
    // 栅栏开启需要的到达线程总数
    private final int parties;
    // 最后一个线程到达后执行的任务
    private final Runnable barrierCommand;
    // 剩余未到达的线程总数
    private int count;
    // 当前轮次的运行状态
    private Generation generation = new Generation();

    // ...
}

我们可以看到其内部依赖了ReentrantLock 和Condition。这里需要注意的是generation变量,它一个静态内部类定义:

// 定义内部类
private static class Generation {
		// broken标识栅栏是否损坏,false标识没有
        boolean broken = false;
    }
// 唤醒等待的下一组线程
private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }    

说明:CyclicBarrier 是可以循环复用的,所以CyclicBarrier 的每一轮任务都需要对应一个generation 对象。generation 对象内部有个broken字段,用来标识当前轮次的CyclicBarrier 是否已经损坏。nextGeneration方法用来创建一个新的generation 对象,并唤醒所有等待线程,重置内部参数。

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

阻塞操作await()实际调用的是内部的dowait()方法,具体实现如下:

private int dowait(boolean timed, long nanos)throws InterruptedException, BrokenBarrierException, TimeoutException {
        final ReentrantLock lock = this.lock;
        // 获取独占锁(lock)
        lock.lock();
        try {
        	// 保持当前的generation
            final Generation g = generation;
			// 若当前栅栏损坏,则抛出异常
            if (g.broken)
                throw new BrokenBarrierException();
            // 如果当前线程被中断,则通过breakBarrier()终止CyclicBarrier,唤醒CyclicBarrier中所有等待线程。
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
			// 将处在等待状态的线程个数count减一
            int index = --count;
            if (index == 0) {  //如果index=0,则意味着当前线程为最后一个等待线程。
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run(); // 执行唤醒后的任务
                    ranAction = true;
                    nextGeneration(); // 唤醒所有等待的线程,并开始下一轮
                    return 0;
                } finally {
                    if (!ranAction) // 若任务执行失败,唤醒所有等待的线程
                        breakBarrier();
                }
            }
            // 自旋,使当前线程一直阻塞,直到满足栅栏条件或 当前线程被中断”或 超时这3者之一发生才继续执行
            for (;;) {
                try {
                    if (!timed) // // 如果不是超时等待,则调用awati()进行等待;否则,调用awaitNanos()进行限时等待。
                        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()终止CyclicBarrier,唤醒CyclicBarrier中所有等待线程。
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

说明:从dowait() 方法的实现逻辑,可以从以下几点看出CyclicBarrier 如何实现阻塞和唤醒:
1.dowait() 方法作用是阻塞当前线程,直到满足栅栏条件(没有等待的线程了)或当前线程被中断,或等待超时,当前线程才会继续执行;
2.在CyclicBarrier中,同一批的线程属于同一代,即同一个Generation;CyclicBarrier中通过generation对象来记录属于哪一代。当所有线程唤醒后会重置栅栏开启需要的到达线程总数;
3.若当前线程被中断,即Thread.interrupted()为true时,会通过breakBarrier()终止CyclicBarrier。同时唤醒所有的等待线程;
4.通过计数器count做自减操作,若满足栅栏开启条件,则会执行后续动作,同时唤醒所有等待线程,初始化计数器;
5.采用自旋操作阻塞当前线程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值