CountDownLatch原理简介和使用过程

前言

本文介绍下面试的高能考点 countDownLatch 的原理和应用

countDownLatch具有的功能

  • CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。

  • 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)

  • 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。

应用场景

典型的应用场景

如并行计算,当某个处理的运算量很大时,可以将该运算任务拆分成多个子任务,等待所有的子任务都完成之后,父任务再拿到所有子任务的运算结果进行汇总。

原理简介

CountDownLatch原理简介和使用过程_第1张图片

上图解释

  • TA线程 实例化 CountDownLatch对象并初始count为3

  • TA调用await 使得当前线程等待count为0 类似于看视屏的时候按了暂停键

  • 另外一个T1线程调用了该CountDownLatch对象的countDown方法 使得count变为2

  • T2线程使得count变为1

  • T3线程使得count变为0

  • 此时就会唤醒处于“暂停”状态的TA线程让其继续往下执行

进一步说明

  • CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。

  • CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。

  • 调用该类await方法的线程会一直处于阻塞状态,直到其他线程调用countDown方法使当前计数器的值变为零,每次调用countDown计数器的值减1。

  • 当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。

  • 这种现象只会出现一次,因为计数器不能被重置,如果业务上需要一个可以重置计数次数的版本,可以考虑使用CycliBarrier

DEMO代码分析

这个示例包含了 线程池和CountDownLatch 的内容 ,更加符合实际使用场景

CountDownLatch原理简介和使用过程_第2张图片

CountDownLatch原理简介和使用过程_第3张图片

详细说明上述示例

针对CountDownLatch的说明

  • 初始化一个CountDownLatch 初始值count=5

  • 调用countDownLatch方法 count+-1即 count=count-1 每次调用count值都会减少1个

  • 调用await方法 就是让初始化CountDownLatch的线程等待count值变为0 然后才会继续执行下面的内容

结合线程池对CountDownDatch的说明

  • 主线程创建了一个count为5的CountDownLatch

  • 主线程创建了一个线程池 里面有5个核心线程

  • 往线程池中提交了5个线程并且传入countDownLatch对象到子线程中去

  • 每一个子线程执行时会调用countDown方法将count减1

  • 主线程调用await 等待count变为0

  • count变为0 主线程将继续执行

源码分析

java.util.concurrent.CountDownLatch 

分析源码的过程中大家把关注点放在我截图上圈红的地方 然后咱们慢慢的一点一点的把源码看完

CountDownLatch原理简介和使用过程_第4张图片

这里将count值设置给state变量 调用了AQS类中setState方法

CountDownLatch原理简介和使用过程_第5张图片

这个变量被volatile修饰
保证了这个变量 具有
1、可见性 
2、有序性 防止指令重排序
3、原子性

这里先简单介绍下可见性

CountDownLatch原理简介和使用过程_第6张图片

上图解释

  • 每一个线程都会有一个本地内存 比如图中线程A有本地内存A 该内存中保留了一份主内存中共享变量的副本

  • 线程A对本地内存A中的共享变量的副本修改了之后 然后会立刻同步刷新到主内存中

  • 并且会让强制缓存了该变量的线程中的数据清空

  • 必须从主内存中重新读取最新的数据

接着说CountDownLatch源码分析

在初始化CountDownLatch的时候会实例化这个继承了AQS的内部类Sync并且将count传给AQS的state变量

CountDownLatch原理简介和使用过程_第7张图片

countDownLatch的await方法 调用了 AQS的acquireSharedInterruptibly方法并且传入了参数1 接下来对该方法分析

CountDownLatch原理简介和使用过程_第8张图片

『tryAcquireShared』
在共享模式下尝试获取。这个方法需要查询是否对象的状态允许在共享模式下被获取,如果允许则去获取它。
这个方法总是被线程执行获取共享锁时被调用。如果这个方法报告失败,那么获取方法可能会使线程排队等待,如果它(即,线程)还没入队的话,直到其他的线程发出释放的信号。
默认实现抛出一个“UnsupportedOperationException”
返回:
a)< 0 : 一个负数的返回表示失败;
b) 0 : 0表示在共享模式下获取锁成功,但是后续的获取共享锁将不会成功
c)> 0 : 大于0表示共享模式下获取锁成功,并且后续的获取共享锁可能也会成功,在这种情况下后续等待的线程必须检查是否有效。


看下AQS的子类Sync的tryAcquireShared方法的实现

CountDownLatch原理简介和使用过程_第9张图片

CountDownLatch原理简介和使用过程_第10张图片

这个逻辑过程中使用了大量的CAS来进行原子性的修改,当修改失败的时候,是会通过for(;;)来重新循环的,也就是说『doAcquireSharedInterruptibly』使用自旋锁(自旋+CAS)来保证在多线程并发的情况下,队列节点状态也是正确的以及在等待队列的正确性,最终使得当前节点要么获取共享锁成功,要么被挂起等待唤醒

我们需要一个通知信号,主要是因为当前线程要被挂起了(park)。
而如果waitStatus已经是’SIGNAL’的话就无需修改,直接挂起就好,
而如果waitStatus是’CANCELLED’的话,说明prev已经被取消了,是个无效节点了,那么无需修改这个无效节点的waitStatus,而是需要先找到一个有效的prev。
因此,剩下的情况就只有当waitStatus为’0’和’PROPAGAET’了(注意,waitStatus为’CONDITION’是节点不在等待队列中,所以当下情况waitStatus不可能为’CONDITION’),这时我们需要将prev的waitStatus使用CAS的方式修改为’SIGNAL’,而且只有修改成功的情况下,当前的线程才能安全被挂起。
还值得注意的时,因此该方法的CAS操作都是没有自旋的,所以当它操作完CAS后都会返回false,在外层的方法中会使用自旋,当发现返回的是false时,会再次调用该方法,以检查保证有当前node有一个有效的prev,并且其waitStatus为’SIGNAL’,在此情况下当前的线程才会被挂起(park)。

CountDownLatch原理简介和使用过程_第11张图片

CountDownLatch原理简介和使用过程_第12张图片

双向链表数据结构

源码分析未完待续 下篇文章继续分析

DEMO代码链接

https://gitee.com/pingfanrenbiji/myconcurrent/tree/master/src/main/java/pers/hanchao/concurrent/eg14

参考文章

https://www.jianshu.com/p/9ee0194d598c

本文使用 mdnice 排版

你可能感兴趣的:(CountDownLatch原理简介和使用过程)