Java `CountDownLatch` 和 `CyclicBarrier` 的区别和使用 -- ISS(Ideas Should Spread)

本文是笔者 Java 学习笔记之一,旨在总结个人学习 Java 过程中的心得体会,现将该笔记发表,希望能够帮助在这方面有疑惑的同行解决一点疑惑,我的目的也就达到了。欢迎分享和转载,转载请注明出处,谢谢合作。由于笔者水平有限,文中难免有所错误,希望读者朋友不吝赐教,发现错漏我也会及时更新修改,欢迎斧正。(可在文末评论区说明或索要联系方式进一步沟通。)

在 Java 5 之前线程的并发和同步要依靠 waitnotify 方法来进行,并且对于复杂的需求使用 waitnotify 可能会导致管理上的麻烦,因此在 Java 5 开始,JDK 引入了许多控制线程并发和同步的工具,CountDownLatchCyclicBarrier 就是其中的两个类(位于 java.util.concurrent 包)。由于新引入的工具类功能强大并且便于使用,因此《Effective Java》作者强烈建议我们优先使用这些工具而不是 waitnotify,当然对于接管遗留代码的程序员掌握 waitnotify 是必要的。下面就 CountDownLatchCyclicBarrier 这两个类进行总结。

先来看一下 JDK 中 CountDownLatchCyclicBarrier 两个类的文档:

  • CountDownLatch 类的部分文档:

    /**
     * A synchronization aid that allows one or more threads to wait until
     * a set of operations being performed in other threads completes.
     *
     * <p>A {@code CountDownLatch} is initialized with a given <em>count</em>.
     * The {@link #await await} methods block until the current count reaches
     * zero due to invocations of the {@link #countDown} method, after which
     * all waiting threads are released and any subsequent invocations of
     * {@link #await await} return immediately.  This is a one-shot phenomenon
     * -- the count cannot be reset.  If you need a version that resets the
     * count, consider using a {@link CyclicBarrier}.
     * ... 
     * ...
     */
     /**
      * Constructs a {@code CountDownLatch} initialized with the given count.
      *
      * @param count the number of times {@link #countDown} must be invoked
      *        before threads can pass through {@link #await}
      * @throws IllegalArgumentException if {@code count} is negative
      */
     public class CountDownLatch {
        ...
        public CountDownLatch(int count) {
            if (count < 0) throw new IllegalArgumentException("count < 0");
            this.sync = new Sync(count);
        }
        ...
    }
    
  • CyclicBarrier 类的部分文档:

    /**
    * A synchronization aid that allows a set of threads to all wait for
    * each other to reach a common barrier point.  CyclicBarriers are
    * useful in programs involving a fixed sized party of threads that
    * must occasionally wait for each other. The barrier is called
    * <em>cyclic</em> because it can be re-used after the waiting threads
    * are released.
    *
    * <p>A {@code CyclicBarrier} supports an optional {@link Runnable} command
    * that is run once per barrier point, after the last thread in the party
    * arrives, but before any threads are released.
    * This <em>barrier action</em> is useful
    * for updating shared-state before any of the parties continue.
    * ...
    */
    /**
    * Creates a new {@code CyclicBarrier} that will trip when the
    * given number of parties (threads) are waiting upon it, and which
    * will execute the given barrier action when the barrier is tripped,
    * performed by the last thread entering the barrier.
    *
    * @param parties the number of threads that must invoke {@link #await}
    *        before the barrier is tripped
    * @param barrierAction the command to execute when the barrier is
    *        tripped, or {@code null} if there is no action
    * @throws IllegalArgumentException if {@code parties} is less than 1
    */
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
    

CountDownLatchCyclicBarrier 的区别

CountDownLatch

  • CountDownLatch 允许一个或多个线程等待一些特定的操作完成,而这些操作是在其它的线程中进行的,也就是说在 CountDownLatch 中,会出现等待的线程被等的线程这样分明的角色;
  • CountDownLatch 构造函数中有一个 count 参数,表示有多少个线程需要被等待,对这个变量的修改是在线程中调用 CountDownLatchcountDown 方法,每一个不同的线程调用一次 countDown 方法就表示有一个被等待的线程到达,count 变为 0 时,latch(门闩)就会被打开,处于等待状态的那些线程接着可以执行;
  • CountDownLatch 是一次性使用的,也就是说latch门闩只能只用一次,一旦latch门闩被打开就不能再次关闭,将会一直保持打开状态,因此 CountDownLatch 类也没有为 count 变量提供 set 的方法;
  • 如果需要可重用的 CountDownLatch,考虑使用 CyclicBarrier

CyclicBarrier

  • CyclicBarrier 允许一系列线程相互等待对方到达一个点,正如 barrier 表示的意思,该点就像一个栅栏,先到达的线程被阻塞在栅栏前,必须等到所有(在这个栅栏注册的)线程都到达了才能够通过栅栏;
  • CyclicBarrier 持有一个变量 parties,表示需要全部到达的线程数量;
  • 率先到达的线程调用 barrier.await 方法进行等待,一旦到达的线程数达到 parties 变量所指定的数,栅栏打开,所有线程都可以通过;
  • CyclicBarrier 构造方法接受另一个 Runnable 类型参数 barrierAction,该参数表明再栅栏被打开的时候需要采取的动作,null 表示不采取任何动作,注意该动作将会在栅栏被打开而所有线程接着运行前被执行;
  • CountDownLatch 不同的是,CyclicBarrier 是可重用的,当最后一个线程到达的时候,栅栏被打开,所有线程通过之后栅栏重新关闭,进入下一代;
  • CyclicBarrier.reset 方法能够手动重置栅栏,此时正在等待的线程会收到 BrokenBarrierException 异常。

CountDownLatch 的使用例子

以下例子来自《Java Concurrency in Practice》,用来计算一个任务列表运行的时间。

1 public class CountDownLatchDemo {
2   public static void main (String[] args) throws IOException, InterruptedException {
3       final int            count = 10;
4       final CountDownLatch entry = new CountDownLatch (1);
5       final CountDownLatch exit  = new CountDownLatch (count);
6       for (int i = 0; i < count; i++) {
7           new Thread (() -> {
8               try {
9                   entry.await ();
                    // doTask()
10              } catch (InterruptedException e) {
11                  e.printStackTrace ();
12              } finally {
13                  exit.countDown ();
14              }
15          }).start ();
16      }
17      final long start = System.nanoTime ();  
18      entry.countDown ();
19      exit.await ();
20      final long end = System.nanoTime ();
21      System.out.println ("Time elapsed: " + (end - start));
22  }
23}

* 其中 4 5 行定义两个 CountDownLatch,一个作为开始计时的启动器,一个作为结束计时的启动器,很明显开始启动计时应该要在所有任务线程都创建完毕,但还不能执行时,而结束计时的启动器应该在最后一个线程运行完由主线程停止;
* 第 6 到 16 行由主线程创建 10 个线程用于运行待测量的任务,但是这些线程 run 方法第一句话为 entry.await ,即线程一开始就在入口等待,还不能开始运行;
* 第 17 行主线程启动计时器后马上调用 entry.countDown 方法,导致 CountDownLatch 的内部变量 count 变为 0,使得其它所有阻塞在 entry.await 这行处的所有任务线程开始执行;
* 而同时主线程马上调用 exit.await 方法,导致主线程在此等待其它所有线程运行完 doTask 方法并且在 finally 块中调用 exit.countDown 方法,减少了 exit 内部变量 count 的计数;
* 所有线程都运行完后 count 变为 0 ,主线程在 20 行处被唤醒,接着记录下结束的时间。

CyclicBarrier 的使用例子

假设模拟一个运动场上赛跑的过程,有一个裁判,很明显裁判应该在所有运动员都就位之后,发起开始的信号(BANG~~~~),在这个例子中,每一个运动员都应该并发地跑(而不是串行的),我们为每个运动员新建一个线程,但是裁判要在所有运动员都就绪的时候发出信号,我们知道 CyclicBarrier 就有这样的功能,所以我们可以使用 CyclicBarrier 作为 “裁判”

public class MultiThread {
    public static void main (String[] args) throws IOException, InterruptedException {

        final int           athleteNum = 20;
        final CyclicBarrier barrier = new CyclicBarrier (athleteNum, () -> System.out.println ("BANG"));
        for (int i = 0; i < athleteNum; i++) {
            new Thread () {
                @Override
                public void run () {
                    try {
                        barrier.await ();
                        System.out.println ("Athlete " + Thread.currentThread ().getId () + " is running");
                    } catch (InterruptedException e) {
                        e.printStackTrace ();
                        System.out.println ("Athlete " + Thread.currentThread ().getId () + "is interrupted");
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace ();
                        System.out.println ("Referee broken");
                    }
                }
            }.start ();
            Thread.sleep (500);
        }
    }
}
  • 其中首先定义运动员的数量为 athleteNum = 20 ,在新建 CyclicBarrier 的时候将它作为第一个参数传进去(毕竟裁判要知道有多少人参赛),CyclicBarrier 第二个参数表示在所有线程就绪后 CyclicBarrier 应该做什么,很明显应该开枪 BANG 示意比赛开始;
  • 接着主线程创建 athleteNum 也就是 20 个线程代表 20 个运动员,在 run 一开始就调用 barrier.await 方法,也就是说该线程一旦准备就绪就要等待 barrier 打开,创建完毕并且启动他们 .start() 方法;
  • 为了便于观察并不是每个运动员一就位就自己跑自己的(抢跑了喂),主线程每创建一个线程就休眠 500 毫秒,停下来看看有没有人抢跑;
  • 当所有线程创建完毕并且就绪后,barrier 被打开(裁判开抢),所有线程并发运行(所有运动员一起跑);
  • 所以以上代码输出结果应该是:一开始约 10 秒(20 个 500 毫秒)内没有输出,此时所有线程都在准备中,10 秒后所有线程都打印出来 Athlete 号码 is running,以下是一种可能的输出:

    BANG
    Athlete 30 is running
    Athlete 11 is running
    Athlete 13 is running
    Athlete 16 is running
    Athlete 12 is running
    Athlete 17 is running
    Athlete 15 is running
    Athlete 14 is running
    Athlete 25 is running
    Athlete 28 is running
    Athlete 24 is running
    Athlete 23 is running
    Athlete 22 is running
    Athlete 21 is running
    Athlete 20 is running
    Athlete 19 is running
    Athlete 18 is running
    Athlete 29 is running
    Athlete 27 is running
    Athlete 26 is running
    
  • 注意到程序中的两个异常捕获,一个是 InterruptedException,在线程就绪完等待 barrier 的过程中(包括通过栅栏后在运行的过程中),如果线程被中断,那么就会抛出该异常;
  • 另一个异常是 BrokenBarrierException,该异常是线程在等待栅栏打开的过程中如果以下情况之一出现就会抛出:
    • 其它同样在等待的线程中有任何一个抛出 InterruptedException
    • 其它同样在等待的线程中有任何一个超时;
    • barrier 被重置(调用 barrier.reset);
    • barrieraction (构造方法中的第二个参数)运行抛出异常;

总结

其实第二个例子完全可以用 CountDownLatch 实现,因为对于栅栏只是使用了一次而已,跟 CountDownLatch 一样。但是试想一下,假设在上面的代码中线程运行过程中真的出现异常了,也就是运动员在跑的过程中出现异常,那么比赛还得重新开始,因此裁判还得重新就位来发出开赛指令。如果使用 CountDownLatch 就没办法重用,而如果使用 CyclicBarrier,可以增加一个标记变量 succeed,外层使用 while(!succeed) 循环,再添加一个 barrier.reset() 的调用,让每一次失败后 barrier 重置。就能达到目的,也体现出 CyclicBarrierCountDownLatch 的区别。

原文地址:https://www.cnblogs.com/keZhenxu94/p/5288476.html