CountDownLatch是AbstractQueuedSynchronizer中共享锁模式的一个的实现,是一个同步工具类,用来协调多个线程之间的同步。CountDownLatch能够使一个或多个线程在等待另外一些线程完成各自工作之后,再继续执行。CountDownLatch内部使用一个计数器进行实现线程通知条件,计数器初始值为进行通知线程的数量。当每一个通知线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的通知线程都已经完成一些任务,然后在CountDownLatch上所有等待的线程就可以恢复执行接下来的任务。基本上CountDownLatch的原理就是这样,下面我们一起去看看源码。

public class CountDownLatch {

    private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L; Sync(int count) {
setState(count);
}
....
} private final Sync sync; public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
....
}

从上面简略的源码可以看出,CountDownLatch和ReentrantLock一样,在内部声明了一个继承AbstractQueuedSynchronizer的Sync内部类(重写了父类的tryAcquireShared(int acquires)和tryReleaseShared(int releases)),并在声明了一个sync属性。CountDownLatch只有一个有参构造器,参数count就是上面说的的进行通知的线程数目,说白点也就是countDown()方法需要被调用的次数。

CountDownLatch的主要方法是wait()和countDown(),我们先看wait()方法源码。

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

和ReentrantLock一样,CountDownLatch依然算是一件外衣,实际还是靠sync进行操作。我们接着看AQS的acquireSharedInterruptibly(int arg)方法(实际上参数在CountDownLatch里没什么用)

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

看到先判断当前线程是否是中断状态,然后调用子类重写的tryAcquireShared(int acquires)方法去判断是否需要进行阻塞(也即是尝试获取锁),如果返回值小于0 ,就调用doAcquireSharedInterruptibly(int acquires)方法进行线程阻塞。先看tryAcquireShared(int acquires)方法

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

源码很简单,就是看下state是否等于0,等于0,就返回1,代表不需要线程阻塞,不等于0(实际上state只会大于或者等于0),就返回-1,表示需要进行线程阻塞。这里有个伏笔就是如果CountDownLatch的计数器state被减至0时,后续再有线程调用CountDownLatch的wait()方法时,会直接往下执行调用者方法的代码,不会造成线程阻塞

private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

在doAcquireSharedInterruptibly(int acquires)方法中进行线程阻塞的步骤依然是先调用addWaiter(Node mode)方法将该线程封装到AQS内部的CLH队列的Node.SHARE(共享)模式的Node节点,并放入到队尾,然后在循环中去尝试持有锁和进行线程阻塞。在循环体内,先获取到前任队尾,然后判断前任队尾是否是队首head,如果是就调用tryAcquireShared(int acquires)尝试获取锁,如果返回1表示获取到了锁,就调用setHeadAndPropagate(Node node, int propagate)方法将节点设置head然后再往下传播,解除后续节点的线程阻塞状态(这就是共享锁的核心)。如果返回-1,表示没有获取到锁,就调用shouldParkAfterFailedAcquire(Node pre, Node node)进行pre节点的waitStatus赋值为Node.SIGNAL,然后在墨迹一次循环,调用parkAndCheckInterrupt()方法进行线程阻塞。我们先看setHeadAndPropagate(Node node, int propagate)方法源码

private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 释放共享锁,这是这一步是最关键的一步
doReleaseShared();
}
}

在setHeadAndPropagate(Node node, int propagate)方法中,直接将node设置从head,因为参数propagate为始终为1(到这一步就表示获取了共享锁,state等于0,在tryAcquireShared(int acquires)方法中就只会返回1),所以也就是后面直接获取head的next节点,如果head的next节点存在,并且是共享模式,就调用doReleaseShared()方法去释放CLH中head的next节点。

shouldParkAfterFailedAcquire(Node pre, Node node)和parkAndCheckInterrupt()两个方法在JUC之ReentrantLock源码分析一篇博客中已经讲过了,这里不再赘述了。

doReleaseShared()这个方法其实也是countDown()方法的核心实现,这里先卖个关子,后面会讲到。我们接着看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;
}
}

当调用wait()方法进行线程阻塞等待被别的线程解除阻塞后,对于AQS中共享锁最核心的代码就是doReleaseShared()这个方法。先获取head节点,如果head节点存在并且有后续节点,就先判断head节点的状态,如果状态是Node.SIGNAL(表示后续节点需要锁释放通知),将head节点状态改为0,然后解除下一节点的线程阻塞状态,然后判断下之前获取的head和现在的head是否还是同一个,如果是就结束循环,如果不是,那就接着循环。其实就算是存在线程在执行完unparkSuccessor(Node node)方法后失去了CPU的执行权,一直到被解除线程阻塞的next节点坐上了head节点后才有机会获取到CPU执行权这种情况,无非就是之前获取到head和现在的head不相同了,大不了再循环一次,也算是多线程去解除当前head节点的next节点线程阻塞,影响不大;如果状态是0,尝试将状态有0改为Node.PROPAGATE,然后重复循环,head节点是0的这种状态在CountDownLatch中应该是不会出现的,可能是AQS对别的类的兼容,也可能是我眼拙没看出来。如果head为null或者head与tail相同,就结束循环。

到这里CountDownLatch的wait()方法就分析完成了,这里总结下wait()方法流程:
  1、如果state是0,直接往下执行调用者的代码,不会进行线程等待阻塞
  2、将当前线程封装到共享模式的Node节点中,然后放入到CLH队列的队尾
  3、将前任队尾的waitStatus改变为Node.SIGNAL,然后调用LockSupport.park()阻塞当前线程,等待前节点唤醒
  4、被前节点唤醒后,把自己设为head,然后去唤醒next节点

我们看完了wait()方法,现在在来看下countDown()方法的源码

public void countDown() {
sync.releaseShared(1);
}

一如既往的简单,直接调用AQS的releaseShared(int arg)方法,我们直接去看AQS的releaseShared(int arg)方法

AQS方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
} CountDownLatch方法
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;
}
}

在AQS的releaseShared(int arg)中先去调用一定要被子类重写的tryReleaseShared(int releases)方法,返回值表示是否需要进行唤醒操作,如果返回true,那就调用doReleaseShared()方法去解除head节点的next节点线程阻塞状态。是的,你没看错,就是我们前面分析的doReleaseShared()方法,他们复用了同一个方法。而在CountDownLatch的tryReleaseShared(int releases)方法实现也非常简单,就是判断下当前state是否是0,如果是0,表示已经减完了,不需要再减了,等待线程已经在被依次唤醒了,返回false表示不需要去唤醒后续节点了。最后再看看减完后的state是否是等于0,等于0,表示需要去解除后续节点的阻塞状态;不等于0(其实是大于0),表示调用countDown()方法去减state的次数还不够,暂时还不能解除后续节点的阻塞状态。

countDown()方法比较简单,我们也总结下countDown()流程:
  1、如果state为0,表示已经有线程在解除CLH队列节点的阻塞状态了,这里直接结束
  2、如果state减去1后不等于0,表示还要等有线程再次调用countDown()方法进行state减1,这里直接结束
  3、如果state减去1后等于0,表示已经线程调用countDown()方法的次数已经达到最初设定的次数,然后去解除CLH队列上节点的阻塞状态

JUC之CountDownLatch源码分析的更多相关文章

  1. Java - "JUC" CountDownLatch源码分析

    Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例 CountDownLatch简介 CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前 ...

  2. CountDownLatch 源码分析

    CountDownLatch 源码分析: 1:CountDownLatch数据结构 成员变量 Sync类型对象 private final Sync sync; Sync是继承AQS的一个类,Coun ...

  3. concurrent(五)同步辅助器CountDownLatch & 源码分析

    参考文档: https://blog.csdn.net/zxdfc/article/details/52752803 简介 CountDownLatch是一个同步辅助类.允许一个或多个线程等待其他线程 ...

  4. JUC AQS ReentrantLock源码分析

    警告⚠️:本文耗时很长,先做好心理准备,建议PC端浏览器浏览效果更佳. Java的内置锁一直都是备受争议的,在JDK1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6 ...

  5. 并发工具CountDownLatch源码分析

    CountDownLatch的作用类似于Thread.join()方法,但比join()更加灵活.它可以等待多个线程(取决于实例化时声明的数量)都达到预期状态或者完成工作以后,通知其他正在等待的线程继 ...

  6. JUC之ReentrantLock源码分析

    ReentrantLock:实现了Lock接口,是一个可重入锁,并且支持线程公平竞争和非公平竞争两种模式,默认情况下是非公平模式.ReentrantLock算是synchronized的补充和替代方案 ...

  7. java源码-CountDownLatch源码分析

    这次分析CountDownLatch,相信大部分人都用过把! CountDownLatch内部还是Sync对象,还是基础AQS(可见其重要性),首先看一下CountDownLatch初始化,Count ...

  8. Java并发系列[7]----CountDownLatch源码分析

    CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行.它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须 ...

  9. CountDownLatch源码分析

    CountDownLatch.Semaphore(信号量)和ReentrantReadWriteLock.ReadLock(读锁)都采用AbstractOwnableSynchronizer共享排队的 ...

随机推荐

  1. 一站式轻量级框架 Spring

    Spring 简介 Spring 是一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的.Spring 的核心是控制反转(IoC)和面向切面编程(AOP).简单来说,Spring ...

  2. Flutter 步骤进度组件

    ​老孟导读:最近文章更新拖后腿了,一直忙着网站改版的事情,今天总算落地了,全新的Flutter网站即将上线,敬请期待.网站目前收集197个组件的详细用法,还有150多个组件待整理. Stepper S ...

  3. MySQL主从复制,主主复制,半同步复制

    实验环境: 系统:CentOS Linux release 7.4.1708 (Core) mariadb:mariadb-server-5.5.56-2.el7.x86_64 node1:172.1 ...

  4. HTTP 前世今生

    HTTP 协议在我们身边随处可见,只要上网就离不开它.不论是用浏览器还是 App,不论是看新闻.短视频还是听音乐.玩游戏,后面总会有 HTTP 在默默为你服务. Http 协议是怎么来的?最开始是什么 ...

  5. JVM致命错误日志详解

    目录 文件描述 文件位置 文件头 错误信息记录 JVM运行信息 崩溃原因 错误信息 线程描述 线程信息 信号信息 计数器信息 机器指令 内存映射信息 线程堆栈 其他信息 进程描述 线程列表 虚拟机状态 ...

  6. 数据挖掘入门系列教程(十一)之keras入门使用以及构建DNN网络识别MNIST

    简介 在上一篇博客:数据挖掘入门系列教程(十点五)之DNN介绍及公式推导中,详细的介绍了DNN,并对其进行了公式推导.本来这篇博客是准备直接介绍CNN的,但是想了一下,觉得还是使用keras构建一个D ...

  7. visual stdio 2012快捷键

    为什么80%的码农都做不了架构师?>>>   VS2012变化的快捷键:注释::VS2010是(Ctrl+E,C),VS2012是(Ctrl+K, Ctrl+C),实际操作,按住Ct ...

  8. 什么是动态规划?动态规划的意义是什么?https://www.zhihu.com/question/23995189

    阮行止 上海洛谷网络科技有限公司 讲师 intro 很有意思的问题.以往见过许多教材,对动态规划(DP)的引入属于"奉天承运,皇帝诏曰"式:不给出一点引入,见面即拿出一大堆公式吓人 ...

  9. 如何使用badboy录制一个脚本并成功的导入jmeter中?

    前言: 虽然,很多人已经不适用这种方式进行录制脚本了,因为不好维护.但是,还是有一些朋友在刚开始学习的过程中使用badboy. 可能有人会好奇了,人家五一都出去玩了,你在家学习吗?正巧前一阵有粉丝留言 ...

  10. CF1336C Kaavi and Magic Spell

    CF1336C Kaavi and Magic Spell 区间dp 题意 给一个长度为 \(n\) 的字符串 \(S\) 和一个长度为 \(m\) 的字符串\(T\) ,\(1\le m\le n\ ...