CyclicBarrier原理详解

介绍

一种同步辅助工具,允许一组线程都等待彼此到达一个共同的障碍点。这个屏障被称为循环,因为它可以在等待的线程被释放后重新使用,之前分析过CountDownLatch,下面说一下两者的区别:

CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后(这N个线程并不相互等待,谁先完成都可以)才能执行。

CyclicBarrier : N个线程相互等待,任何一个线程没有到达或完成时,所有的线程都必须互相等待

通过一个简单的例子来理解一下CyclicBarrier,例如运动会中运动员在颁奖时,假如其中一个人在登奖台前准备好,颁奖嘉宾是不能进行颁奖的,需要等待冠军,季军,亚军三位人员都准备就绪后颁奖嘉宾才能进行颁奖,那么这三名运动员都准备就绪这就是一个屏障点,只有三名运动员都准备就绪之后,突破屏障点之后颁奖嘉宾才能进行颁奖操作。

banjiang.png

源码解析

主要字段信息

// 用来记录当前屏障是否已经被打破,当broken=true时说明被打破。
private static class Generation {
    boolean broken = false;
}

![banjiang.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eed94a7f9bf64187ac37e2af3e665e97~tplv-k3u1fbpfcp-watermark.image)
/** ReentrantLock锁 */
private final ReentrantLock lock = new ReentrantLock();
/** 锁对应的条件 */
private final Condition trip = lock.newCondition();
/** 需要等待线程总数 */
private final int parties;
/* 当突破屏障后执行的内容 */
private final Runnable barrierCommand;
/**  用来记录当前屏障是否已经被打破,当broken=true时说明被打破,这里没有声明成volatile是因为只有一个线程获取 */
private Generation generation = new Generation();

/**
 * 还需要多少个线程达到屏障点后才能突破屏障。
 */
private int count;

构造函数

CyclicBarrier基于ReentrantLock的独占锁实现,本质其实也是基于AQS实现,每当有线程调用await方法时,会将当前线程挂起放入到AQS的条件队列中,此时count会减少1,当count减少到0时,表示所有线程已经达到了屏障点,执行通过构造函数传递过来的任务。首先先来看一下构造函数,CyclicBarrier提供了两个构造函数,如下所示:

/**
 * 指定相互等待线程数量,并由最后一个进入屏障的线程来执行barrierAction。
 */
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

/**
 * 指定相互等待线程。
 */
public CyclicBarrier(int parties) {
    this(parties, null);
}

通过构造函数清晰看到有三个变量,count、parties、barrierCommod,其中barrierCommod是通过屏障之后需要执行的任务,为啥会有两个数量?而且两个数量是一样的,大家都知道CyclicBarrier是个可以复用的,如果只设计一个count,当线程调用await时,count进行减1操作,就没办法进行复用操作,parties始终用来记录总的个数,count用来控制还需要多少个线程到达屏障点,当count减少到0时,说明所有线程已经到达了屏障点,parties会重新将值赋值给count值,从而进行复用操作。

await方法

接下来就来分析一下await内部是如何进行操作的?先上源码,如下所示:

public int await() throws InterruptedException, BrokenBarrierException {
    try {
      	// 内部调用了dowait方法
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

通过源码得知,await其实调用了内部的dowait方法来进行线程等待操作,当抛出异常后会直接抛出Error异常信息。

/**
 * 主要障碍代码。
 */
private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        final Generation g = generation;
	// 判断broken是否为true,如果为true说明屏障被打破了,直接抛出异常即可。
        if (g.broken)
            throw new BrokenBarrierException();
	// 响应中断,当线程被中断时,说明屏障被打破了,通知其他线程,当其他线等待的线程再获取到锁时,会直接抛出异常。
        if (Thread.interrupted()) {
            // 设置broken为true,并通知所有线程等待的线程。
            breakBarrier();
            throw new InterruptedException();
        }
	// count进行减少。
        int index = --count;
      	// 当count减少到0时,运行构造函数传递过来的barrierCommand屏障任务。
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    // 运行方法。
                    command.run();
                ranAction = true;
              	// 通知所有线程突破屏障,循环下一次屏障。
                nextGeneration();
                return 0;
            } finally {
                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 {
                    // 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)
                return index;
	    // 如果等待时间小于0抛出异常。
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}
  1. 首先上来先进行申请锁操作,也就是同时只有一个线程来进行操作,其他线程需要假如到AQS的等待队列中进行等待。

  2. 判断broken是否为true,如果为true说明屏障被打破了,直接抛出异常即可,这个是在什么时候会被打破,通过源码可以看到

    • 当前线程相应中断请求
    • 如果指定了等待时间时,并且传递等待时间是负数,则也会打破屏障。
    • 执行构造函数barrierCommand传递过来的任务报错时,也会打破屏障。
  3. 判断count是否已经减少到0,表示所有线程已经达到了屏障点,由最后一个线程来执行通过构造函数传递过来的barrierCommand任务,调用nextGeneration方法,进行下一轮复用的重置动作。

    private void nextGeneration() {
        // 通知所有等待队列中的线程。
        trip.signalAll();
        // 重置count值。
        count = parties;
        // 新建一个generation对象,这里为啥要新建一个呢?我们往下面看。
        generation = new Generation();
    }
    
    • 通知所有条件队列中等待的线程,条件队列中被唤醒的线程,必须先进行获取锁的操作,先将唤醒的线程会被加入到AQS的等待队列中,并获取锁操作,当有线程获取到锁后,会到await的地方继续执行。
    • 当执行到 if (g != generation) return index; 此时另外一一个线程已经将generation设置为新的对象,所以会直接返回。
  4. breakBarrier是如何控制打破屏障的?

    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }
    
    • 将generation的broken设置为true,count设置为parties。
    • 唤醒所有的线程,唤醒所有线程的时候会执行第61行的代码 if (g.broken) throw new BrokenBarrierException();这时候if语句中为true,则会直接抛出异常打破屏障。

总结

  • 一种同步辅助工具,允许一组线程都等待彼此到达一个共同的障碍点。
  • 这个屏障被称为循环,因为它可以在等待的线程被释放后重新使用。
  • 内部使用的依然是AQS,不过比起CountDownLatch来讲,CyclicBarrier是内部自己实现的,而CountDownLatch利用的AQS同步锁state状态值来控制。
  • CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后(这N个线程并不相互等待,谁先完成都可以)才能执行。
  • CyclicBarrier : N个线程相互等待,任何一个线程没有到达或完成时,所有的线程都必须互相等待

喜欢的朋友可以关注我的微信公众号,不定时推送文章

123.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值