笔记 8 · CyclicBarrier全面解析

##应用场景与 countDownLatch差不多。

一种同步辅助工具,允许一组线程都等待彼此到达一个共同的障碍点。cyclicbarrier在涉及固定大小的线程组的程序中很有用,这些线程偶尔必须等待对方。这个屏障被称为循环的,因为它可以在等待的线程被释放后重新使用

例如一个团队游戏,总共10人参加,其中有一个项目是推到高墙,必须10个人一起参加,缺一不可。那么先到达墙下的人必须等待,等人齐后一起推。这10个人就相当于10个线程。

CyclicBarrier支持一个可选的Runnable命令,该命令在参与方中的最后一个线程到达之后,但在释放任何线程之前,在每个屏障点运行一次。此屏障操作有助于在任何一方继续之前更新共享状态。

CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。
在这里插入图片描述

比较

countDownLatch:
1、主任务要等待所有的子任务完成后,在执行主任务
2、计数器归零后,就不会在变了。只能设置一重屏障。
3、是基于 AQS 的共享模式的使用。
4、计数器则由使用者来控制,线程调用await方法只是将自己阻塞而不会减少计数器的值。

cyclicBarrier:
1、假设有5个线程,其中4个线程都到达了某个点(屏障处),他们会陷入等待,直到第5个线程也到达了这个点,所有的线程才会继续往下执行。主线程不会被阻塞。
2、数量是可以重用。线程冲破屏障后,会恢复到原来的初始值(构造方法传入的参数)。可以实现多重屏障。
3、是基于 Condition 来实现的。
4、计数器由自己(代码逻辑)控制,线程调用await方法不仅会将自己阻塞还会将计数器减1。

至此我们难免会将CyclicBarrier与CountDownLatch进行一番比较。这两个类都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候所有阻塞的线程将会被唤醒。
在这里插入图片描述

方法

private static class Generation { boolean broken = false; }

  • 静态内部类Generation
  • 起到多重屏障的作用。Barrier被冲破或重置的时候,generation会发生变化。
  • Generation描述着CyclicBarrier的更新换代。在CyclicBarrier中,同一批线程属于同一代。当有parties个线程到达barrier之后,generation就会被更新换代。其中broken标识该当前CyclicBarrier是否已经处于中断状态。

public CyclicBarrier(int parties, Runnable barrierAction) { … }

  • 创建一个新的CyclicBarrier,它将在给定数量的参与方(线程)等待它触发
  • 并在屏障被触发时执行给定的屏障操作,由最后一个进入屏障的线程执行屏障。
  • ( barrierAction 支持一个可选的Runnable命令,该命令在参与方中的最后一个线程到达之后,但在释放任何线程之前,在每个屏障点运行一次。此屏障操作有助于在任何一方继续之前更新共享状态。)

public CyclicBarrier(int parties) { this(parties, null); }

  • parties 参数方,设置冲破屏障的线程个数
  • 并且在屏障被触发时不执行预定义的操作。

public int getParties() { … }

  • 返回跳过此障碍所需的参与方数。

public int await() throws InterruptedException, BrokenBarrierException {…}

  • 等待直到所有的 parties(参与方),都已经在屏障之前调用了await()方法。
  • 如果当前线程不是最后到达的线程,则它将处于休眠状态,直到发生以下情况之一:
  • 1、直到所有的 parties(参与方),都已经在屏障之前调用了await()方法;
  • 2、其他线程中断当前线程;
  • 3、其他线程中断其他等待线程之一;
  • 4、其他线程在等待屏障时超时;
  • 5、其他线程对此屏障调用reset

public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {…}

  • 等待,直到所有参与方都对此屏障调用了wait,或者指定的等待时间已过。
  • 如果当前线程不是最后到达的线程,则它将处于休眠状态,直到发生以下情况之一:
  • 1、直到所有的 parties(参与方),都已经在屏障之前调用了await()方法;
  • 2、其他线程中断当前线程;
  • 3、其他线程中断其他等待线程之一;
  • 4、其他线程在等待屏障时超时;
  • 5、其他线程对此屏障调用reset
  • 6、指定的超时将被延迟;

public boolean isBroken() { … }

  • 查询此屏障是否处于断开状态。
  • return generation.broken

public void reset() { … }

  • 将屏障重置为其初始状态。如果有任何一方目前正在关卡等待,他们将返回BrokenBarrier。
  • 请注意,由于其他原因发生中断后的重置可能会很复杂;线程需要以其他方式重新同步,然后选择一种方式来执行重置。更可取的做法是创建一个新的屏障以供后续使用。

public int getNumberWaiting() { … }

  • 返回当前在屏障处等待的参与方数(parties - count)。此方法主要用于调试和断言。

成员变量

private final ReentrantLock lock = new ReentrantLock();

  • The lock for guarding barrier entry
  • 起到多重屏障的作用。Barrier被冲破或重置的时候,generation会发生变化。

private final Condition trip = lock.newCondition();

  • Condition to wait on until tripped
  • 屏障状态(线程拦截器)

private final int parties ;

  • The number of parties
  • 每次屏障前需要拦截的线程总数

private final Runnable barrierCommand ;

  • The command to run when tripped
  • 屏障被冲破时,需要运行的命令

private Generation generation = new Generation();

  • The current generation
  • 表示栅栏的当前代(当前generation)

private int count ;

  • 仍在等待的参与方数量,起到计数器的作用。
  • 每当有一个线程到达屏障前,count都会减1。直到减到0,线程冲破屏障。
  • 之后开始下一个分代,会重新将parties的值赋给count。

案例

一层屏障

案例描述:开启3个线程,每个线程完成的时间不同。所有的线程都完成后,屏障打开。

public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep((long)(Math.random() * 2000));
                    int randomInt = new Random().nextInt(500);
                    System.out.println("hello" + randomInt);
                    cyclicBarrier.await();
                    System.out.println("world" + randomInt);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    // 输出信息
    // hello318
    // hello464
    // hello436
    // world436
    // world318
    // world464
双层屏障

案例描述:循环实现双重屏障
在这里插入图片描述

public static void main(String[] args) {
    CyclicBarrier cyclicBarrier = new CyclicBarrier(1);
    for (int j = 0; j < 2; j++) {
        for (int i = 0; i < 1; i++) {
            new Thread(() -> {
                ...
            }).start();
        }
    }
}
// 输出信息
// j = 0  hello207
// world207
// j = 1  hello451
// world451

源码分析

底层执行流程

  1. 初始化cyclicBarrier中的各种成员变量,包括parties、count以及Runnable(可选)。
  2. 当调用await方法时,底层先检查计数器(count)是否已经归零,是的话,那么就首先执行可选的Runnable,接下开始下一个generation(分代、下一重屏障);
  3. 在下一个分代中,将会重置count值为parties,并且创建新的Generation实例。
  4. 同时会调用Condition的signalAll方法,唤醒所有在屏障前等待的线程,让其继续执行。
  5. 如果计数器没有归零,那么当前的调用线程将会通过Condition的await方法,在屏障前进行等待。
  6. 以上所有执行流程均在lock锁控制范围内,不会出现并发情况。

在CyclicBarrier所有的成员变量中,可以看到CyclicBarrier内部是通过条件队列trip来对线程进行阻塞的,并且其内部维护了两个int型的变量parties和count,parties表示每次拦截的线程数,该值在构造时进行赋值。count是内部计数器,它的初始值和parties相同,以后随着每次await方法的调用而减1,直到减为0就将所有线程唤醒。CyclicBarrier有一个静态内部类Generation,该类的对象代表栅栏的当前代,就像玩游戏时代表的本局游戏,利用它可以实现循环等待。barrierCommand表示换代前执行的任务,当count减为0时表示本局游戏结束,需要转到下一局。在转到下一局游戏之前会将所有阻塞的线程唤醒,在唤醒所有线程之前你可以通过指定barrierCommand来执行自己的任务。我用一图来描绘下 CyclicBarrier 里面的一些概念:
在这里插入图片描述

首先从构造函数出发

【CyclicBarrier.class# 构造器1
public CyclicBarrier(int parties, Runnable barrierAction) {
  if (parties <= 0) throw new IllegalArgumentException();
  this.parties = parties;
  this.count = parties;
  this.barrierCommand = barrierAction;
}
 
# 构造器2
public CyclicBarrier(int parties) {
  this(parties, null);
}

# CyclicBarrier有两个构造器,其中构造器1是它的核心构造器,在这里你可以指定本局游戏的参与者数量(要
# 拦截的线程数: parties)以及本局结束时要执行的任务(barrierAction),还可以看到计数器count的初始值
# 被设置为parties。

再看 await()

【CyclicBarrier.class# CyclicBarrier类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的
# 方法,分别是定时等待和非定时等待。

# 非定时等待
public int await() throws InterruptedException, BrokenBarrierException {
  try {
    return dowait(false, 0L);
  } catch (TimeoutException toe) {
    throw new Error(toe);
  }
}
 
# 定时等待
public int await(long timeout, TimeUnit unit) throws InterruptedException, 
	BrokenBarrierException, TimeoutException {
  return dowait(true, unit.toNanos(timeout));
}

# 可以看到不管是定时等待还是非定时等待,它们都调用了dowait方法,只不过是传入的参数不同而已。
# 下面我们就来看看dowait方法都做了些什么。

# 核心等待方法
# 逻辑处理比较简单,如果该线程不是最后一个调用await方法的线程,则它会一直处于等待状态,
# 除非发生以下情况:
# 1、最后一个线程到达,即index == 0
# 2、某个参与线程等待超时
# 3、某个参与线程被中断
# 4、调用了CyclicBarrier的reset()方法。该方法会将屏障重置为初始状态
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()) {
      //如果当前线程被中断会做以下三件事
      //1.打翻当前栅栏
      //2.唤醒拦截的所有线程
      //3.抛出中断异常
      breakBarrier();
      throw new InterruptedException();
    }
    # 每次dowait()都将计数器的值减1
    int index = --count;
    # 计数器的值减为0则需唤醒所有线程并转换到下一代
    if (index == 0) {
      boolean ranAction = false;
      try {
        # 唤醒所有线程前先执行指定的任务
        final Runnable command = barrierCommand;
        if (command != null) {
          command.run();
        }
        ranAction = true;
        # 唤醒所有线程并转到下一代
        nextGeneration();
        return 0;
      } finally {
        # 确保在任务未成功执行时能将所有线程唤醒
        if (!ranAction) {
          breakBarrier();
        }
      }
    }
 
    # 如果计数器不为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 {
          # 上面条件不满足,说明这个线程不是这代的。不会影响当前这代栅栏的执行,所以,打个中断标记
          Thread.currentThread().interrupt();
        }
      }
      # 如果线程因为打翻栅栏操作而被唤醒则抛出异常
      if (g.broken) {
        throw new BrokenBarrierException();
      }
      # 如果线程因为换代操作而被唤醒则返回计数器的值。
      # g != generation表示正常换代了,返回当前线程所在栅栏的下标
      # 如果 g == generation,说明还没有换代,那为什么会醒了?
      # 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是
      # 当前代。正是因为这个原因,才需要generation来保证正确。
      if (g != generation) {
        return index;
      }
      # 如果线程因为时间到了而被唤醒则打翻栅栏并抛出异常
      if (timed && nanos <= 0L) {
        breakBarrier();
        throw new TimeoutException();
      }
    }
  } finally {
    lock.unlock();
  }
}

上面贴出的代码中注释都比较详细,我们只挑一些重要的来讲。可以看到在dowait方法中每次都将count减1,减完后立马进行判断看看是否等于0,如果等于0的话就会先去执行之前指定好的任务,执行完之后再调用nextGeneration方法将栅栏转到下一代,在该方法中会将所有线程唤醒,将计数器的值重新设为parties,最后会重新设置栅栏代次,在执行完nextGeneration方法之后就意味着游戏进入下一局。如果计数器此时还不等于0的话就进入for循环,根据参数来决定是调用trip.awaitNanos(nanos)还是trip.await()方法,这两方法对应着定时和非定时等待。如果在等待过程中当前线程被中断就会执行breakBarrier方法,该方法叫做打破栅栏,意味着游戏在中途被掐断,设置generation的broken状态为true并唤醒所有线程。同时这也说明在等待过程中有一个线程被中断整盘游戏就结束,所有之前被阻塞的线程都会被唤醒。线程醒来后会执行下面三个判断,看看是否因为调用breakBarrier方法而被唤醒,如果是则抛出异常;看看是否是正常的换代操作而被唤醒,如果是则返回计数器的值;看看是否因为超时而被唤醒,如果是的话就调用breakBarrier打破栅栏并抛出异常。这里还需要注意的是,如果其中有一个线程因为等待超时而退出,那么整盘游戏也会结束,其他线程都会被唤醒。

breakBarrier

dowait ==> breakBarrier

【CyclicBarrier.class# 打翻当前栅栏
private void breakBarrier() {
  # 将当前栅栏状态设置为打翻
  generation.broken = true;
  # 设置计数器的值为需要拦截的线程数
  count = parties;
  # 唤醒所有线程
  trip.signalAll();
}

nextGeneration

dowait ==> nextGeneration

【CyclicBarrier.class# 切换栅栏到下一代
private void nextGeneration() {
  # 唤醒条件队列所有线程
  trip.signalAll();
  # 设置计数器的值为需要拦截的线程数
  count = parties;
  # 重新设置栅栏代次
  generation = new Generation();
}

最后,看怎么重置一个栅栏

【CyclicBarrier.classpublic void reset() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        breakBarrier();   // break the current generation
        nextGeneration(); // start a new generation
    } finally {
        lock.unlock();
    }
}

我们设想一下,如果初始化时,指定了线程 parties = 4,前面有 3 个线程调用了 await 等待,在第 4 个线程调用 await 之前,我们调用 reset 方法,那么会发生什么?

首先,打破栅栏,那意味着所有等待的线程(3个等待的线程)会唤醒,await 方法会通过抛出 BrokenBarrierException 异常返回。然后开启新的一代,重置了 count 和 generation,相当于一切归零了。

注:源码分析内容主要来源晨初听雨

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值