CountDownLatch 是干啥的,到底难不难?

本文详细解析了 Doug Lea 的 CountDownLatch,讲解了其作为AQS框架工具的作用,如何等待线程完成,以及在实际项目中的应用案例。通过源码剖析,揭示了其工作原理和关键方法,适合已熟悉ReentrantLock和Semaphore的读者快速掌握。
摘要由CSDN通过智能技术生成

一、闲说题外话

Doug Lea 很多并发的工具类。其中 AQS 框架,最为有名,面试问的也最多, 为啥呢?

ConCurrentHashMapDoug Lea 写的,

面试都问烂了,就算没看过源码,也能编几句,给糊弄过去。

真要是看过源码,里面大量位运算的代码,不好描述,面试官会听晕的。

ThreadPoolExecutor 也是 Doug Lea 写的,

同样是面试必问,原理很复杂,仅仅是说清楚源码就不容易了,

细节点很多,你要是看过源码,找一个冷门的,能把面试官给问死。

这多尴尬,面试官也不会太往深处问。

BlockingQueue 也是 Doug Lea 写的,

它是线程池的底层之一,面试官很喜欢问常见的队列有哪些,

你要是回答出来了,会再问你哪些是阻塞队列,哪些是有界队列,

其实这都是皮毛,看点文章就能背出来。底层原理是 ReentrantLock

还有就是 Condition,这两个工具类都算是 基于 AQS 框架实现的。

是的,不用问,都是 Doug Lea 写的 !!!
在这里插入图片描述
这位就是传说中的大神——Doug Lea,一看就是文化人的样子。

和蔼可亲,风度翩翩,尤其是那飘逸的头发,让我等程序员好生羡慕呀!!

哎呀,跑题啦!还说技术

AQS 框架下,有几个重量级的工具类,ReentrantLockSemaphoreCountDownLatch 等等。

这些面试官就特别喜欢问,原理较简单,几句话就能说清楚,

会就是会,不会就是不会,立马见水准。

AQS框架,平日程序员写业务代码,不常用,但一旦用了,那肯定是有一定水平的程序员。

能说出点源码细节的,说明这人来时有进取心,会看些高并发的东东。

就像中学生,一说起鲁迅的文章,都会头发麻,

那些所谓的深意,鲁迅当时真的想到了吗?

程序员看到高并发的代码,也会头皮发麻,

简单说就是搞不定,说白了,就是对 Doug Lea 提供的工具类,不熟悉。

说多了都是泪呀!!!!

对大神我是特别的膜拜,就算是要把我给煮了,

我也写不出这牛气冲天的,高并发工具类。

然而,然而,然而,大神写的源码,我仔仔细细看了,

而且,而且,而且,看完我还写了源码的详细解读!

让我叉会儿腰,我骄傲!!! 上面的红色字里,加有超链接哈,

点开就是我写的源码分析,陆陆续续写的源码解读哈!

今天咱们就来说说 CountDownLatch

二、言归正传,CountDownLatch 到底是什么?

先看下官方的解释


 * A synchronization aid that allows one or more threads to wait until
 * a set of operations being performed in other threads completes.
 

字面意思就是:

CountDownLatch 是一个并发的工具类,

可以让某个线程等待,等待啥呢?

等待其它线程执行结束,这个线程再开始执行。

这个解释很生硬,说一个现实的场景,你可以体会下。

说一个导游,带了10个人参观景点。导游说自由参观哈,一个小时后,上车去下个景点。

快一个小时了,游客陆陆续续回来了,导游点下人数,人不齐,给司机说等会再出发,

又过了一会,最后一个游客回来了,导游说人齐了,司机大哥开车走人。

这里司机开车到下个景点,你可以认为是一个线程要做的事,

10个游客参观景点,完事回到车上,可以认为是其它 10 个线程。

开车那个线程,要等待 游客的10个线程都结束了,即回到车上,司机才会开车。

否则就一直等待。

CountDownLatch 实现这样的功能,让某个线程一直等待,直到其它线程结束。

三、示例用法

CountDownLatch的源码,除去注释,总共就没几行,原理也很简单,我就多扯些别的。

其实在源码的注释中,已经很详细的交待了,怎么用,还给了例子。

之前我写了一篇《Semaphore 信号量探究》,里面的示例代码,

就很好的体现了 CountDownLatch 的用法。

这里我给出一个,我在工作中用到的一个例子。

一个接口里,我要查询 5 张表的数据,返回给页面,假设这5张表之间,没有依赖,

即 查询的先后顺序不影响结果,那正常就是查 5 次数据库。

这样整个接口在耗时就比较长。想要缩短耗时,打算用多线程的方法查。

保证所有子线程查询结束后,再结束接口的请求。


  @SneakyThrows
  public static void main(String[] args) {
      int count = 4;
      ExecutorService pool = Executors.newFixedThreadPool(count);
      CountDownLatch countDownLatch = new CountDownLatch(count);
      pool.execute(() -> {
          selectOne();// 业务代码,查询表 1 
          countDownLatch.countDown();
      });
      pool.execute(() -> {
          selectTwo();// 业务代码,查询表 2 
          countDownLatch.countDown();
      });
      pool.execute(() -> {
          selectThree();// 业务代码,查询表 3 
          countDownLatch.countDown();
      });
      pool.execute(() -> {
          selectFour();// 业务代码,查询表 4 
          countDownLatch.countDown();
      });
      selectFive(); // 查表5由主线程执行
      countDownLatch.await();
    }

这只是有 demo,这里能说明问题就好。

五个任务,开了4个子线程,每个线程执行一个,最后一个,由主线程执行。

插一个问题:五个任务,为什么只开了4个线程?

这里使用的线程池,忽略创建线程的时间,execute 方法的执行时间,是 0.01 毫秒 这个级别的。

连接数据库,查询操作,相对是耗时操作,是 毫秒 级别,或是上 百毫秒 级别的。

大概画了张图来说明问题
在这里插入图片描述

用多线程来查询,显然比五个线程来执行五个任务来的快。

多线程版本,假设五个任务中,耗时最长的是 15 毫秒,那总任务耗时大约就是 15 毫秒,

而单线程版本,耗时大概等于,五个任务耗时的总和。

countDownLatch 的作用,就是等子线程任务都执行完,才让主线程将结果返回。

四、原理图解

countDownLatch 也是 AQS 框架的一员,AQS 对象中,有个 state 属性。

在这里插入图片描述

在示例代码中,初始化时,state 会被设置为 4,(因为 count 的值是4)

CountDownLatch countDownLatch = new CountDownLatch(count);

执行 countDownLatch.countDown() 时,state 会相应减 1。

当主线程执行了到 countDownLatch.await(); 这行时,

假设子线程中,只有任务3没有执行完

即线程3还没有调用 countDown() 这个方法,

那此时 state 等于 1,那 await 会被阻塞线程。

  • 为什么 state 等于 1?
    初始化时,state 等于 4,刚刚我说了,只有任务3没有执行完,
    那也就是说,其它三个线程执行完了,调用 countDown() 这个方法
    调用一次 countDown() ,state 就减 1

  • 为什么state 等于 1时,await 会阻塞线程?
    await 方法执行时,会判断 state 的值,若等于0,方法结束,
    若大于0,阻塞线程(调用 park 方法),等待被唤醒。

至此为止,简单说明了下,主线程等待子线程是怎么实现的。

总结下就是:

  1. 初始化时,state 会被赋值
  2. 子线程调用 countDown() 方法时,会将 state 减 1
  3. 主线程调用 await() 方法时,若 state 大于 0 ,线程会被阻塞

这就实现了主线程等待子线程。

在这里插入图片描述
当任务 3 执行完,调用了 countDown() 方法时,state 被 减小 1,此时 state 等于 0,

这种情况下,countDown() 内部还会调用 unpark 方法唤醒主线程

关于 parkunpark ,不熟悉的,可以看《阻塞与唤醒

至此,解释了阻塞的线程,是什么时候被唤醒的。

countDownLatch 的使用,及其原理,大概就说完了。

开篇插了个问题,5 个任务,为什么只开了4个线程?

了解 await 方法后,你能想出原因么,欢迎在评论区,给出你的想法。

五、源码略讲

  • countDown 方法,就是将 state 减 1.
 
  public void countDown() {
      sync.releaseShared(1);
  }

  public final boolean releaseShared(int arg) {
      if (tryReleaseShared(arg)) {
          doReleaseShared();
          return true;
      }
      return false;
  }


      protected boolean tryReleaseShared(int releases) {
          // Decrement count; signal when transition to zero
          for (;;) {
              int c = getState();
              if (c == 0)
                  return false;
              int nextc = c-1;
              if (compareAndSetState(c, nextc))
                  return nextc == 0;
          }
      }

tryReleaseShared 这个方法比较简单,就是 将 state 减 1,

doReleaseShared 这方法等会再说。

  • 下面看 await 方法

  public void await() throws InterruptedException {
      sync.acquireSharedInterruptibly(1);
  }

  public final void acquireSharedInterruptibly(int arg)
          throws InterruptedException {
      if (Thread.interrupted())
          throw new InterruptedException();
      if (tryAcquireShared(arg) < 0)
          doAcquireSharedInterruptibly(arg);
  }

     protected int tryAcquireShared(int acquires) {
         return (getState() == 0) ? 1 : -1;
     }

从源码中看出,当 state 等于 0 时,tryAcquireShared 返回 1,

tryAcquireShared(arg) < 0 这个条件就不成立,await 方法就结束了。

state 不等于0 时,会执行 doAcquireSharedInterruptibly(arg);

这个方法,在《Semaphore 信号量》 这篇中,具体讲过,这里不讲了。

你想从源码层面理解,翻看这篇文章就好了,反正本文是不细讲了。

只用知道,这个方法会调用 park 方法,使线程阻塞。

简单总结下:await 方法是,判断 state 是否等于0,不等于0 阻塞,等于 0 结束。

  • 下面再说下,阻塞之后是怎么唤醒的

前面讲 countDown 时说过,当 state - 1 等于 0 时,

会执行 doReleaseShared ,这个就是唤醒线程的。


private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

这里 unparkSuccessor 就是唤醒线程。

这个源码,也不细讲了,同样在 《Semaphore 信号量》 详细讲过。

至此为止,CountDownLatch 大概的说了一遍。

讲的很粗略,不是我糊弄人,因为以前的文章,相关的源码已经分析过了。

如果你对 ReentrantLockSemaphore 很了解,

可是说已经看了我写的文章,

CountDownLatch 真的不用再看源码,知道怎么用就可以了。

不信你试试哈!!!

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值