Java并发基础六:并发工具类(2)CyclicBarrier

CyclicBarrier是什么

CyclicBarrier是JDK1.5开始提供的并发编程,辅助工具类。用于并发编程的。在源码中使用 ReentrantLock 和 Condition 的组合来使用,CyclicBarrier字面意思是“可重复使用的栅栏”。通过它可以实现让一组线程等待至某个状态之后再全部同时执行利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。

CyclicBarrier工作原理

看下API
CyclicBarrier有两个构造函数

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)

第一个参数,表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
第二个参数,表示用于在线程到达屏障时,优先执行barrierAction这个Runnable对象,方便处理更复杂的业务场景。

让线程处于barrier状态的方法await()

public int await()
public int await(long timeout, TimeUnit unit)

第一个默认方法,表示要等到所有的线程都处于barrier状态,才一起执行
第二个方法,指定了等待的时间,当所有线程没有都处于barrier状态,又到了指定的时间,所在的线程就继续执行了。

实现原理:1、首先在CyclicBarrier的内部定义了一个ReentrantLock对象,调用lock获取锁,同时将为获得锁的线程放入阻塞队列并挂起。2、然后定义一个count值,每当一个获取锁的线程调用CyclicBarrier的await方法时,count值减1同时将该线程加入到Condition条件队列中,3、加入队列成功后,调用tryRelease()函数释放锁同时唤醒(LockSupport.unpark)阻塞队列中下一个节点,紧接着将放入阻塞队列中的线程挂起。4、然后阻塞队列中唤醒的线程获取锁重试上面动作,直到count值等于0时,如果传递了barrierAction这个Runnable对象,将先执行该线程,5、barrierAction执行完后,Condition.signalAll唤醒所有线程并转到下一代,6、唤醒的所有线程将同时进行后续任务的执行,(即当前被唤醒的所有线程调用acquireQueued去抢占同步锁,节点会从 condition 队列移动到 AQS 等待队列,则进入正常锁的获取流程)CyclicBarrier的await方法后面的流程就可以继续执行下去了。

上面实现原理将全部流程都表述完了,不太好理解,小白建议查看前面AQS文章,使用通俗易懂的话在表达一下:

白话原理:首先在CyclicBarrier的内部定义了一个ReentrantLock对象 和 计数器count值,每当一个获取锁的线程调用CyclicBarrier的await方法时,count值减1同时将该线程挂起,直到count值等于0时,将唤醒挂起的所有线程,可以同时进行后续的操作了。

看如下示意图,CyclicBarrier 和 CountDownLatch 很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。

cyclicbarrier-2

CyclicBarrier应用场景例子
Demo:报旅行团旅行,三人成团导游统一办理签证即可旅行,可以有很多这样的团

public class CyclicBarrierDEmo {
    /**
     * 模拟旅行社旅游,三人开团同时旅行之前导游统一办理签证后,大家即可开启旅程
     */
    static class TravelTask implements Runnable{
        /**
         * 游客旅行线程
         */
        private CyclicBarrier cyclicBarrier;
        private String name;
        private int arriveTime;//赶到的时间

        public TravelTask(CyclicBarrier cyclicBarrier, String name, int arriveTime){
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
            this.arriveTime = arriveTime;
        }

        @Override
        public void run() {
            try {
                //模拟达到需要花的时间
                Thread.sleep(arriveTime * 1000);
                System.out.println(Thread.currentThread().getName()+ name +"到达集合点");
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName()+ name +"开始旅行啦~~");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception{

        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
            /**
             * 导游线程,都到达目的地时,办理签证
             */
            @Override
            public void run() {
                System.out.println("****导游办理签证****");
                try {
                    //模拟发护照签证需要2秒
                    Thread.sleep(3000);
                    System.out.println("****签证办理完成了,可以一起出发了****");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Executor executor = Executors.newFixedThreadPool(3);
        //登哥最大牌,到的最晚
        for (int i =0;i<3;i++) {
            executor.execute(new TravelTask(cyclicBarrier, "哈登"+i, 3));
            executor.execute(new TravelTask(cyclicBarrier, "保罗"+i, 2));
            executor.execute(new TravelTask(cyclicBarrier, "戈登"+i, 1));
        }
    }
}

CyclicBarrier源码解析

激动人心的时刻到了,一起看下源码实现吧!

首先,CyclicBarrier 的源码实现和 CountDownLatch 大相径庭,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的。因为 CyclicBarrier 的源码相对来说简单许多,读者只要熟悉了前面关于 Condition 的分析,那么这里的源码是毫无压力的,就是几个特殊概念罢了。

在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理,下面我们先看看CyclicBarrier有哪些成员变量。

//同步操作锁
private final ReentrantLock lock = new ReentrantLock();
//线程拦截器
private final Condition trip = lock.newCondition();
//每次拦截的线程数
private final int parties;
//换代前执行的任务
private final Runnable barrierCommand;
//表示栅栏的当前代
private Generation generation = new Generation();
//计数器
private int count;
 
//静态内部类Generation
private static class Generation {
  boolean broken = false;
}

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

接下来我们看看它的构造器。

//构造器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是它的核心构造器,在这里你可以指定本局游戏的参与者数量(要拦截的线程数)以及本局结束时要执行的任务,还可以看到计数器count的初始值被设置为parties。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方法都做了些什么。

//核心等待方法
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();
    }
    //每次都将计数器的值减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();
      }
      //如果线程因为换代操作而被唤醒则返回计数器的值
      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打破栅栏并抛出异常。这里还需要注意的是,如果其中有一个线程因为等待超时而退出,那么整盘游戏也会结束,其他线程都会被唤醒。下面贴出nextGeneration方法和breakBarrier方法的具体代码。

//切换栅栏到下一代
private void nextGeneration() {
  //唤醒条件队列所有线程
  trip.signalAll();
  //设置计数器的值为需要拦截的线程数
  count = parties;
  //重新设置栅栏代次
  generation = new Generation();
}
 
//打翻当前栅栏
private void breakBarrier() {
  //将当前栅栏状态设置为打翻
  generation.broken = true;
  //设置计数器的值为需要拦截的线程数
  count = parties;
  //唤醒所有线程
  trip.signalAll();
}

trip.await()具体代码:

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
}

   final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
public final boolean release(int arg) {
        // 将锁释放同时State值设置为0
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 头结点ws值为0,并唤醒下个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

最后,我们来看看怎么重置一个栅栏:

public 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,相当于一切归零了。

线程唤醒后,则继续执行下面的方法获取锁(执行顺序可参考前篇文章:深入理解AQS),最后执行await下面的代码。

final boolean acquireQueued(final Node node, int arg) {
 boolean failed = true;
 try {
    boolean interrupted = false;
    for (;;) {
       final Node p = node.predecessor();//获取当前节点的 prev 节点
       if (p == head && tryAcquire(arg)) {//如果是 head 节点,说明有资格去争抢锁
          setHead(node);//获取锁成功,也就是ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权限
          p.next = null; //把原 head 节点从链表中移除
          failed = false;
          return interrupted;
      }
      //ThreadA 可能还没释放锁,使得 ThreadB 在执行 tryAcquire 时会返回 false
      if (shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt())
          interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
   }
 } finally {
    if (failed)
      cancelAcquire(node);
    }
}

文章参考:

https://blog.csdn.net/qq_39241239/article/details/87030142

https://www.jianshu.com/p/4ef4bbf01811

https://www.jianshu.com/p/3b92bd4b430a

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章