多线程并发之CyclicBarrier(栅栏)使用详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/J080624/article/details/85261930

【1】CyclicBarrier简介

CyclicBarrier,是JDK1.5的java.util.concurrent并发包中提供的一个并发工具类。翻译为中文为“篱栅”意思,也就是栅栏。

① Javadoc

CyclicBarrier是一个同步辅助类,它允许一组线程相互等待直到所有线程都到达一个公共的屏障点。CyclicBarrier在涉及一定大小的线程的程序而这些线程有时必须彼此等待的情况下很有用。这个屏障之所以用循环修饰,是因为在所有的线程释放之后,这个屏障是可以重新使用的。

CyclicBarrier支持可选择的Runnable命令(实例),在团队中的最后一个线程到达之后,但是在释放任何线程之前,每个屏障点运行一次(执行Runnable命令的run方法)。在内部任何一个线程继续之前,这种屏障操作对于更新共享状态是有用的。

下面是在并行分解设计中使用屏障的示例:

public class Solver {
    final int N;
    final float[][] data;
    final CyclicBarrier barrier;

    class Worker implements Runnable {
      int myRow;
      Worker(int row) { myRow = row; }
      
      public void run() {
        while (!done()) {
          processRow(myRow);
          try {
            barrier.await();
          } catch (InterruptedException ex) {
            return;
          } catch (BrokenBarrierException ex) {
            return;
          }
        }
      }
    }

    public Solver(float[][] matrix) {
      data = matrix;
      N = matrix.length;
      Runnable barrierAction =new Runnable() { public void run() { mergeRows(...); }};
      barrier = new CyclicBarrier(N, barrierAction);

      List<Thread> threads = new ArrayList<Thread>(N);
      for (int i = 0; i < N; i++) {
        Thread thread = new Thread(new Worker(i));
        threads.add(thread);
        thread.start();
      }
      // wait until done
      for (Thread thread : threads)
        thread.join();
    }
}

上面实例中每个工作线程处理矩阵的一行,然后在屏障处等待,直到所有行都被处理。当所有行被处理时,将执行提供的{Runnable}屏障操作并合并这些行。If the merger determines that a solution has been found then {@code done()} will return {@code true} and each worker will terminate.

如果屏障操作在执行时不依赖于被挂起的各方,那么当释放该操作时,该方中的任何线程都可以执行该操作。为了实现这一点,每次调用{await}都会返回该线程在屏障处的到达索引。然后您可以选择哪个线程应该执行屏障操作,例如:

//执行在屏障处等待索引为0的线程
if (barrier.await() == 0) {
 // log the completion of this iteration
}}

CyclicBarrier对失败的同步尝试使用全或无中断模型:如果某个线程由于中断、故障或超时而过早地离开屏障点,那么在该屏障点等待的所有其他线程也将通过{BrokenBarrierException}异常离开(或者{InterruptedException}如果它们同时被中断)。

② CyclicBarrier特性总结

CyclicBarrier可以使一定数量的线程在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

CyclicBarrier支持在线程执行前首先执行一个Runnable Command。

如果某个线程出了问题(如InterruptedException),那么将会影响其他线程。

CyclicBarrier使用ReentranLock进行加锁,使用Condition的await、signal及signalAll方法进行线程间通信。


【2】CyclicBarrier类详解

① 属性和内部类

//一代,默认broken为false
	private static class Generation {
	        boolean broken = false;
	}
    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();
    
    /** The number of parties */
    private final int parties;
    
    /* The command to run when tripped */
    private final Runnable barrierCommand;
    
   //当前代
    private Generation generation = new Generation();

    /**
     * Number of parties still waiting. Counts down from parties to 0
     * on each generation.  It is reset to parties on each new
     * generation or when broken.
     */
    private int count;

② 构造函数

CyclicBarrier内部使用了ReentrantLock和Condition两个类。它有两个构造函数:

//带有Runnable barrierAction
public CyclicBarrier(int parties, Runnable barrierAction) {
       if (parties <= 0) throw new IllegalArgumentException();
       this.parties = parties;
       this.count = parties;
       this.barrierCommand = barrierAction;
   }
//不带Runnable barrierAction
public CyclicBarrier(int parties) {
        this(parties, null);
    }

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

CyclicBarrier的另一个构造函数CyclicBarrier(int parties, Runnable barrierAction),用于线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。


③ 核心方法await与dowait

调用await方法的线程告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞。直到parties个参与线程调用了await方法,CyclicBarrier同样提供带超时时间的await和不带超时时间的await方法:

//无参
public int await() throws InterruptedException, BrokenBarrierException {
    try {
         return dowait(false, 0L);
     } catch (TimeoutException toe) {
         throw new Error(toe); // cannot happen
     }
}
   //传入了TimeUnit 
public int await(long timeout, TimeUnit unit)
    throws InterruptedException,
           BrokenBarrierException,
           TimeoutException {
    return dowait(true, unit.toNanos(timeout));
}

其最终都是调用dowait()方法。

//下一代
private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
}
//破坏栅栏
private void breakBarrier() {
    generation.broken = true;
    count = parties;
    trip.signalAll();
}
private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
            TimeoutException {
    // 获取独占锁--ReentrantLock 
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
        // 当前代
        final Generation g = generation;
        // 如果这代损坏了,抛出异常
        if (g.broken)
            throw new BrokenBarrierException();
 
        // 如果线程中断了,抛出异常
        if (Thread.interrupted()) {
            // 将损坏状态设置为true,并通知其他阻塞在此栅栏上的线程
            breakBarrier();
            throw new InterruptedException();
        }
 
        // count-1,获取下标
        int index = --count;
        // 如果是 0,说明最后一个线程调用了该方法
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                // 执行栅栏任务
                if (command != null)
                    command.run();
                ranAction = true;
                // 更新一代,将count重置,将generation重置
                // 唤醒之前等待的线程
                nextGeneration();
                return 0;
            } finally {
                // 如果执行栅栏任务的时候失败了,就将损坏状态设置为true
                if (!ranAction)
                    breakBarrier();
            }
        }
 	//正常情况下,线程将会在这里死循环,然后阻塞到这里,直到被唤醒或者意外发生
        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                 // 如果没有时间限制,则直接等待,直到被唤醒
                if (!timed)
                    trip.await();//condition通信
                // 如果有时间限制,则等待指定时间
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                // 当前代没有损坏
                if (g == generation && ! g.broken) {
                    // 让栅栏失效
                    breakBarrier();
                    throw ie;
                } else {
 // 上面条件不满足,说明这个线程不是这代的,就不会影响当前这代栅栏的执行,所以,就打个中断标记
                    Thread.currentThread().interrupt();
                }
            }
 
            // 当有任何一个线程中断了,就会调用breakBarrier方法
            // 就会唤醒其他的线程,其他线程醒来后,也要抛出异常
            if (g.broken)
                throw new BrokenBarrierException();
 
            // g != generation表示正常换代了,返回当前线程所在栅栏的下标
            // 如果 g == generation,说明还没有换代,那为什么会醒了?
            // 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。
            // 正是因为这个原因,才需要generation来保证正确。
            if (g != generation)
                return index;
            
            // 如果有时间限制,且时间小于等于0,销毁栅栏并抛出异常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        // 释放独占锁
        lock.unlock();
    }
}

dowait(boolean, long)方法的主要逻辑处理比较简单,如果该线程不是最后一个调用await方法的线程,则它会一直处于等待状态,除非发生以下情况:

  • 最后一个线程到达,即index == 0
  • 某个参与线程等待超时
  • 某个参与线程被中断
  • 调用了CyclicBarrier的reset()方法。该方法会将屏障重置为初始状态

在上面的源代码中,我们可能需要注意Generation 对象,在上述代码中我们总是可以看到抛出BrokenBarrierException异常,那么什么时候抛出异常呢?

如果一个线程处于等待状态时,如果其他线程调用reset(),或者调用的barrier原本就是被损坏的,则抛出BrokenBarrierException异常。同时,任何线程在等待时被中断了,则其他所有线程都将抛出BrokenBarrierException异常,并将barrier置于损坏状态。

同时,Generation描述着CyclicBarrier的更新换代。在CyclicBarrier中,同一批线程属于同一代。当有parties个线程到达barrier之后,generation就会被更新换代。其中broken标识该当前CyclicBarrier是否已经处于中断状态。

private static class Generation {
       boolean broken = false;
   }

默认barrier是没有损坏的。当barrier损坏了或者有一个线程中断了,则通过breakBarrier()来终止所有的线程:

private void breakBarrier() {
    generation.broken = true;
    count = parties;
    trip.signalAll();
}

在breakBarrier()中除了将broken设置为true,还会调用signalAll将在CyclicBarrier处于等待状态的线程全部唤醒。

当所有线程都已经到达barrier处(index == 0),则会通过nextGeneration()进行更新换代操作,在这个步骤中,做了三件事:唤醒所有线程,重置count,generation:

private void nextGeneration() {
    // signal completion of last generation
    trip.signalAll();
    // set up next generation
    count = parties;
    generation = new Generation();
}

除了上面讲到的栅栏更新换代以及损坏状态,我们在使用CyclicBarrier时还要要注意以下几点:

  • CyclicBarrier使用独占锁来执行await方法,并发性可能不是很高
  • 如果在等待过程中,线程被中断了,就抛出异常。但如果中断的线程所对应的CyclicBarrier不是这代的,比如,在最后一次线程执行signalAll后,并且更新了这个“代”对象。在这个区间,这个线程被中断了,那么,JDK认为任务已经完成了,就不必在乎中断了,只需要打个标记。该部分源码已在dowait(boolean, long)方法中进行了注释。
  • 如果线程被其他的CyclicBarrier唤醒了,那么g肯定等于generation,这个事件就不能return了,而是继续循环阻塞。反之,如果是当前CyclicBarrier唤醒的,就返回线程在CyclicBarrier的下标。完成了一次冲过栅栏的过程。该部分源码已在dowait(boolean, long)方法中进行了注释。

④ reset方法

将屏障重置为初始状态。如果有线程当前在屏障处等待,则它们将返回{BrokenBarrierException}。注意,由于其他原因中断发生后重置可能执行起来很复杂;线程需要以其他方式重新同步,并选择一个来执行重置。相反,最好创建一个新的屏障供以后使用。

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

⑤ getNumberWaiting

获取当前在屏障前等待的线程数量:

public int getNumberWaiting() {
  final ReentrantLock lock = this.lock;
   lock.lock();
   try {
       return parties - count;
   } finally {
       lock.unlock();
   }
}

⑥ isBroken
获取是否破损标志位broken的值,此值有以下几种情况:

  • CyclicBarrier初始化时,broken=false,表示屏障未破损。
  • 如果正在等待的线程被中断,则broken=true,表示屏障破损。
  • 如果正在等待的线程超时,则broken=true,表示屏障破损。
  • 如果有线程调用CyclicBarrier.reset()方法,则broken=false,表示屏障回到未破损状态。
public boolean isBroken() {
     final ReentrantLock lock = this.lock;
     lock.lock();
     try {
         return generation.broken;
     } finally {
         lock.unlock();
     }
 }

【3】CyclicBarrier实例

CyclicBarrier核心主要有两点:线程组内彼此相互等待,然后大家开始做某件事;这一代结束后开始下一代–重用思想。

CountDownLatch核心思想为确保某些活动直到其他活动都完成才继续执行,而且CountDownLatch不可重用。

举例,班级集体野炊,在大巴上等待所有同学到来才开始出发;一个班级集合完毕出发一辆大巴,这是CyclicBarrier。到达目的地后需要同学自行寻找食材,寻找到需要的所有食材后才开始做饭,一个班级做饭活动是一次性的,这是CountDownLatch。

① 测试屏障线程

即创建CyclicBarrier时传入Runnable Command。

 public static void main(String[] args) throws InterruptedException {
        //构造器:设置屏障放开前做的事情
        CyclicBarrier barrier3 = new CyclicBarrier(2, () -> {
            LOGGER.info("屏障放开,[屏障线程]先运行!");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LOGGER.info("[屏障线程]的事情做完了!");
        });
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                LOGGER.info(Thread.currentThread().getName() + " 等待屏障放开");
                try {
                    barrier3.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                LOGGER.info(Thread.currentThread().getName() + "开始干活...干活结束");
            }).start();
        }
    }

测试结果如下:

Thread-0 等待屏障放开
 Thread-1 等待屏障放开
屏障放开,[屏障线程]先运行!
[屏障线程]的事情做完了!
Thread-1开始干活...干活结束
Thread-0开始干活...干活结束

② CyclicBarrier 等待与复用

宿舍四哥们约着去操场打球:

public class CyclicBarrierDemo {
    private static final ThreadPoolExecutor threadPool=new ThreadPoolExecutor(4,10,60, TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());
    //当拦截线程数达到4时,便优先执行barrierAction,然后再执行被拦截的线程。
    private static CyclicBarrier cb=new CyclicBarrier(4,new Runnable() {
        public void run()
        {
            System.out.println("寝室四兄弟一起出发去球场");
        }
    });
    private static class GoThread extends Thread{
        private final String name;
        public GoThread(String name)
        {
            this.name=name;
        }
        public void run()
        {
            System.out.println(name+"开始从宿舍出发");
            try {
                cb.await();//拦截线程
                System.out.println(name+"从楼底下出发");

            } catch(InterruptedException e){
                e.printStackTrace();
            } catch(BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        String[] str= {"李明","王强","刘凯","赵杰"};
        String[] str1= {"王二","洪光","雷兵","赵三"};
        for(int i=0;i<4;i++)
        {
            threadPool.execute(new GoThread(str[i]));
        }
        try{
            Thread.sleep(4000);
            System.out.println("四个人一起到达球场,现在开始打球");
            System.out.println("现在对CyclicBarrier进行复用.....");
            System.out.println("又来了一拨人,看看愿不愿意一起打:");
        } catch(InterruptedException e)
        {
            e.printStackTrace();
        }
         cb.reset();
        //进行复用:
        for(int i=0;i<4;i++)
        {
            threadPool.execute(new GoThread(str1[i]));
        }
        try {
            Thread.sleep(4000);
            System.out.println("四个人一起到达球场,表示愿意一起打球,现在八个人开始打球");
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        threadPool.shutdown();
    }
}

测试结果如下:

李明开始从宿舍出发
王强开始从宿舍出发
刘凯开始从宿舍出发
赵杰开始从宿舍出发
寝室四兄弟一起出发去球场
赵杰从楼底下出发
李明从楼底下出发
王强从楼底下出发
刘凯从楼底下出发
四个人一起到达球场,现在开始打球
现在对CyclicBarrier进行复用.....
又来了一拨人,看看愿不愿意一起打:
王二开始从宿舍出发
洪光开始从宿舍出发
雷兵开始从宿舍出发
赵三开始从宿舍出发
寝室四兄弟一起出发去球场
赵三从楼底下出发
王二从楼底下出发
雷兵从楼底下出发
洪光从楼底下出发
四个人一起到达球场,表示愿意一起打球,现在八个人开始打球

【4】CyclicBarrier与CountDownLatch

CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景。

CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断。

CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。

CountDownLatch实现CyclicBarrier

CountDownLatch通过将await()方法和countDown()方法在不同线程组分别调用,从而实现线程组间的线程等待,即一个线程组等待另一个线程组执行结束再执行。而CyclicBarrier类则是通过调用await()方法实现线程组内的线程等待,即达到需要拦截的线程数,被拦截的线程才会依次获取锁,释放锁。那么将CountDownLatch的用法进行转换,即在同一个线程组内调用await()方法和countDown()方法,则可实现CyclicBarrier类的功能。但是注意的是必须先调用countDown()方法,才能调用await()方法,因为一旦调用await()方法,该线程后面的内容便不再执行,那么count值无法改变。具体代码如下:

public class CyclicBarrierWithCount {
    private static CountDownLatch cdl=new CountDownLatch(4);
    private final static ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());//使用线程池

    private static class GoThread extends Thread{
        private final String name;

        public GoThread(String name)
        {
            this.name=name;

        }
        public void run()
        {
            System.out.println(name+"开始从宿舍出发");
            cdl.countDown();
            System.out.println(cdl.getCount());
            try
            {
                cdl.await();//拦截线程
                System.out.println(name+"从楼底下出发");
            }
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {
        // TODO Auto-generated method stub
        String[] str= {"李明","王强","刘凯","赵杰"};
        String[] str1= {"王二","洪光","雷兵","赵三"};
        for(int i=0;i<4;i++){
            threadPool.execute(new GoThread(str[i]));
        }
        try {
            Thread.sleep(4000);
            System.out.println("四个人一起到达球场,现在开始打球");
            System.out.println("现在对CyclicBarrier进行复用.....");
            System.out.println("又来了一拨人,看看愿不愿意一起打:");
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        cdl=new CountDownLatch(4);
        for(int i=0;i<4;i++)
        {
            threadPool.execute(new GoThread(str1[i]));
        }
        try {
            Thread.sleep(4000);
            System.out.println("四个人一起到达球场,表示愿意一起打球,现在八个人开始打球");
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        //关闭线程池
        threadPool.shutdown();
    }
}

测试结果如下:

李明开始从宿舍出发
3
王强开始从宿舍出发
2
刘凯开始从宿舍出发
1
赵杰开始从宿舍出发
0
李明从楼底下出发
王强从楼底下出发
刘凯从楼底下出发
赵杰从楼底下出发
四个人一起到达球场,现在开始打球
现在对CyclicBarrier进行复用.....
又来了一拨人,看看愿不愿意一起打:
王二开始从宿舍出发
3
洪光开始从宿舍出发
2
雷兵开始从宿舍出发
1
赵三开始从宿舍出发
0
赵三从楼底下出发
王二从楼底下出发
雷兵从楼底下出发
洪光从楼底下出发
四个人一起到达球场,表示愿意一起打球,现在八个人开始打球

参考博文:

多线程并发之CountDownLatch(闭锁)使用详解
队列同步器AQS-AbstractQueuedSynchronizer 原理分析
CyclicBarrier的基本方法和应用场景实例
CyclicBarrier的工作原理及其实例
Java并发编程之CyclicBarrier详解

猜你喜欢

转载自blog.csdn.net/J080624/article/details/85261930