Loading

CyclicBarrier源码阅读

本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习

本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么

简介

说起CyclicBarrier不得不提一下CountDownLatch,他们最明显的区别就是:

  • CountDownLatch:将线程分为执行者和等待者
  • CyclicBarrier:执行者和等待者都是同一批线程

如何理解呢,CountDownLatch大家都知道,设置一批工作线程,各自执行完任务就调用CountDownLatch.countDown,表示自己已经执行完任务,该工作线程就可以继续干别的事情了。而CyclicBarrier相当于在CountDownLatch.countDown之后设置了一道屏障,自己完成了任务后,还得等别的工作线程也完成了之后才能继续干别的事情。

实际上CountDownLatch就能简单实现一个CyclicBarrier的核心功能:

// 这两句由同一个线程执行
countDownLatch.countDown();
countDownLatch.await();

就是这么简单,在countDown后加个await就行,相当于执行者和等待者都是同一个线程罢了。在我看来CyclicBarrier只是将两条语句合成了一句:barrier.await()

当然,CyclicBarrier还支持了其他的更加完善的机制:

  • 复用:不同于CountDownLatch只能用一次,每次count减到0后就不能再加回来了。CyclicBarrier可以复用,支持多轮等待
  • barrier action:创建CyclicBarrier时可以传入一个Runnable作为barrier action,在最后一个线程到达(await)时,由该线程执行该barrier action,执行完毕之后,所有线程才能被唤醒继续执行
  • breakage:如果在最后一个线程到达之前,正在await的线程因为中断、超时等原因退出了(相当于await提前返回了),那么其他所有正在await的线程都将收到BrokenBarrierException异常。注意barrier action抛异常的话也会造成BrokenBarrierException。如果BrokenBarrierException一旦抛出,除非reset,否则该轮一直处于未完成状态,并且处于breakage状态无法使用。

注意barrier action在最后一个到达的线程await时才执行,执行完毕后所有线程的await才会返回。假如不需要“执行完毕后所有线程的await才会返回”,你只希望在最后一个到达的线程await后所有线程马上得到释放(并且希望action依然由最后一个到达的线程执行),可以这么做:

if (barrier.await() == 0) {
	// action
}

await返回当前线程是第几个到达的线程,0代表最后一个到达(n-1代表第一个, n-2代表第2个...)。

最后,对于breakage发生后的reset,文档建议最好重新new一个CyclicBarrier,而不是reset。首先肯定只能由其中一个线程来reset,既然大家都收到了BrokenBarrierException,谁来reset合适?假如选出了负责reset的那个线程,在其reset之前,其他线程都不能await,这里又存在一个线程间同步问题需要解决。综上,发生breakage后,如果还想继续使用CyclicBarrier,重新new一个是比较好的选择。

代码分析

成员变量

public class CyclicBarrier {
	// Generation代表一轮
  private static class Generation {
    Generation() {}                 
    // 该轮是否broken,即breakage
    boolean broken;
  }
	
  // 保护下面的变量
  private final ReentrantLock lock = new ReentrantLock();
  // 用于唤醒所有等待的线程
  private final Condition trip = lock.newCondition();
  
	// 总线程数
  private final int parties;
	// barrier action
  private final Runnable barrierCommand;
	// 当前轮
  private Generation generation = new Generation();
	// 还没到达的线程数
  private int count;
}

方法

轮次正常或异常结束相关方法:

// 唤醒当前轮所有线程,开启下一轮
private void nextGeneration() {
  trip.signalAll();
  count = parties;
  generation = new Generation();
}
// 将当前轮设置为失败,唤醒当前轮所有线程
private void breakBarrier() {
  generation.broken = true;
  count = parties;
  trip.signalAll();
}

重置:

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

上面的方法比较简单,核心都在doWait方法,这个方法是await方法的实现:

// 以下几种情况会使得dowait返回
// 1. 最后一个线程到达,本轮正常结束,以下其他情况都是本轮broken并抛异常
// 2. 本轮已是broken
// 3. 超时
// 4. 中断
// 5. barrier action抛异常
private int dowait(boolean timed, long nanos)
  throws InterruptedException, BrokenBarrierException,
  TimeoutException {
  final ReentrantLock lock = this.lock;
  // 加锁访问
  lock.lock();
  try {
    final CyclicBarrier.Generation g = generation;
		// 情况2
    if (g.broken)
      throw new BrokenBarrierException();
		// 情况4
    if (Thread.interrupted()) {
      breakBarrier();
      throw new InterruptedException();
    }
		// 还没到达的线程数-1
    int index = --count;
    // 是否最后一个到达
    if (index == 0) {
      Runnable command = barrierCommand;
      // 执行barrier action
      if (command != null) {
        try {
          command.run();
        } catch (Throwable ex) {
					// 情况5
          breakBarrier();
          // 注意,当barrier action抛出异常,本线程抛出的是barrier action的异常
          // 其他线程抛出的是BrokenBarrierException
          throw ex;
        }
      }
      // 执行完毕,本轮成功,开始下一轮
      nextGeneration();
      return 0;
    }

    // 不是最后一个到达的线程
    for (;;) {
      try {
        // 在条件变量上等待
        if (!timed)
          trip.await();
        else if (nanos > 0L)
          nanos = trip.awaitNanos(nanos);
      } catch (InterruptedException ie) {
        // 情况4
        // 如果本轮未结束
        if (g == generation && ! g.broken) {
          breakBarrier();
          throw ie;
        } else {
          // 本轮已经结束(已经下一轮或者已经broken),那么这个中断应该留给下一轮用
          Thread.currentThread().interrupt();
        }
      }

      if (g.broken)
        // 情况2
        throw new BrokenBarrierException();

      if (g != generation)
        // 情况1,本轮成功
        return index;

      if (timed && nanos <= 0L) {
        // 情况3
        breakBarrier();
        throw new TimeoutException();
      }
    }
  } finally {
    // 解锁,退出
    lock.unlock();
  }
}

“不是最后一个到达的线程”的情况下为什么要无限循环执行呢,考虑Condition.await唤醒只有以下几种可能:要么被signal(broken或者新一轮)、要么超时、要么中断,分支基本都覆盖到了,估计是为了兜底,因为说不定Condition.await可能会诈尸,无故唤醒...或是怕别的地方没设计好,可能会来了个不应该来的signal。

参考链接

「StackOverflow」Real Life Examples For CountDownLatch and CyclicBarrier

「Java全栈知识体系」JUC工具类: CyclicBarrier详解

posted @ 2024-01-30 16:56  NOSAE  阅读(14)  评论(0编辑  收藏  举报