并发——深入分析CountDownLatch的实现原理
一、前言
最近在研究java.util.concurrent
包下的一些的常用类,之前写了AQS
、ReentrantLock
、ArrayBlockingQueue
以及LinkedBlockingQueue
的相关博客,今天这篇博客就来写一写并发包下的另一个常用类——CountDownLatch
。这里首先要说明一点,CountDownLatch
是基于AQS
实现的,AQS
才是真正实现了线程同步的组件,CountDownLatch
只是它的使用者,所以如果想要学习CountDownLatch,请一定先要弄懂AQS的实现原理。我以下的描述均建立在已经了解AQS
的基础之上。我之前写过一篇AQS
实现原理的分析博客,感兴趣可以看一看:并发——抽象队列同步器AQS的实现原理。
二、正文
2.1 抽象队列同步器AQS
在说CountDownLatch
前,必须要先提一下AQS
。AQS
全称抽象队列同步器(AbstractQuenedSynchronizer),它是一个可以用来实现线程同步的基础框架。当然,它不是我们理解的Spring
这种框架,它是一个类,类名就是AbstractQuenedSynchronizer
,如果我们想要实现一个能够完成线程同步的锁或者类似的同步组件,就可以在使用AQS
来实现,因为它封装了线程同步的方式,我们在自己的类中使用它,就可以很方便的实现一个我们自己的锁。
AQS
的实现相对复杂,无法通过短短的几句话将其说清楚,我之前专门写过一篇分析AQS
实现原理的博客:并发——抽象队列同步器AQS的实现原理。
在阅读下面的内容前,请一定要先学习AQS的实现原理,因为CountDownLatch
的实现非常简单,完全就是依赖于AQS
的,所以我以下的描述均建立在已经理解AQS
的基础之上。可以阅读上面推荐博客,也可以自己去查阅相关资料。
2.2 CountDownLatch的实现原理
既然已经开始学习CountDownLatch
的实现原理了,那一定已经知道了它的作用,我这里就不详细展示了,简单介绍一下:CountDownLatch
的被称为门栓,可以将它看成是门上的锁,它会给门上多把锁,只有每一把锁都解开,才能通过。对于线程来说,CountDownLatch
会阻塞线程的运行,只有当CountDownLatc
内部记录的值减小为0
,线程才能继续向前执行。
CountDownLatch
底层通过AQS
实现,AQS
的一般使用方式就是以内部类的形式继承它,CountDownLatch
就是这么使用它的。在CountDownLatch
内部有一个内部类Sync
,继承自AQS
,并重写了AQS
加锁解锁的方法,并通过Sync
的对象,调用AQS
的方法,阻塞线程的运行。我们知道,创建一个CountDownLatch
对象时,需要传入一个整数值count
,只有当count
被减小为0
时线程才能通过await
方法,否则将被await
阻塞。这里实际上是这样的:当线程运行到await方法时,需要去获取锁(锁由AQS实现),若count不为0,则线程就会获取锁失败,被阻塞;若count为0,则就能顺利通过。CountDownLatch
是一次性的,因为没有方法可以增加count
的值,也就是说,一旦count
被减小为0
,则之后就一直是0
了,也就再也不能阻塞线程了。下面我们就从源码的角度来分析CountDownLatch
。
2.3 CountDownLatch的内部类
前面我们说过,CountDownLatch
内部定义了一个内部类Sync
,继承自AQS
,通过这个内部类来实现线程阻塞,下面我们就来看一看这个内部类的实现:
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
/** 构造方法,接收count值,只有count减小为0时,线程才不会被await方法阻塞 */
Sync(int count) {
// CountDownLatch利用AQS的方式就是直接让count作为AQS的同步变量state
// 所以直接用state记录count值
setState(count);
}
/** 获取当前的count值 */
int getCount() {
return getState();
}
/**
* 这是AQS的模板方法acquireShared、acquireSharedInterruptibly等方法内部将会调用的方法,
* 由子类实现,这个方法的作用是尝试获取一次共享锁,对于AQS来说,
* 此方法返回值大于等于0,表示获取共享锁成功,反之则获取共享锁失败,
* 而在这里,实际上就是判断count是否等于0,线程能否向下运行
*/
protected int tryAcquireShared(int acquires) {
// 此处判断state的值是否为0,也就是判断count是否为0,
// 若count为0,返回1,表示获取锁成功,此时线程将不会阻塞,正常运行
// 若count不为0,则返回-1,表示获取锁失败,线程将会被阻塞
// 从这里我们已经可以看出CountDownLatch的实现方式了
return (getState() == 0) ? 1 : -1;
}
/**
* 此方法的作用是用来是否AQS的共享锁,返回true表示释放成功,反之则失败
* 此方法将会在AQS的模板方法releaseShared中被调用,
* 在CountDownLatch中,这个方法用来减小count值
*/
protected boolean tryReleaseShared(int releases) {
// 使用死循环不断尝试释放锁
for (;;) {
// 首先获取当前state的值,也就是count值
int c = getState();
// 若count值已经等于0,则不能继续减小了,于是直接返回false
// 为什么返回的是false,因为等于0表示之前等待的那些线程已经被唤醒了,
// 若返回true,AQS会尝试唤醒线程,若返回false,则直接结束,所以
// 在没有线程等待的情况下,返回false直接结束是正确的
if (c == 0)
return false;
// 若count不等于0,则将其-1
int nextc = c-1;
// compareAndSetState的作用是将count值从c,修改为新的nextc
// 此方法基于CAS实现,保证了操作的原子性
if (compareAndSetState(c, nextc))
// 若nextc == 0,则返回的是true,表示已经没有锁了,线程可以运行了,
// 若nextc > 0,则表示线程还需要继续阻塞,此处将返回false
return nextc == 0;
}
}
}
可以看到,内部类Sync的实现非常简单,它只实现了AQS
中的两个方法,即tryAcquireShared以及tryReleaseShared,这两个方法是AQS
提供的使用共享锁的接口。这也就表明,CountDownLatch
实际上是一种共享锁机制,即锁可以同时被多个线程获取,这个不难理解,因为一旦count
被减小为0,则所有线程通过await
方法时,都能够顺利通过,不会因为获取不到锁而阻塞。而且从上面的实现中我们可以看到,Sync
直接将count
值作为AQS
的state
的值,只有state
的值为0,线程才能获取锁,也就是获得执行权限。
2.4 CountDownLatch的成员变量和构造方法
下面来看一看CountDownLatch
的属性和构造方法:
/**
* 只有一个成员变量,就是内部类Sync的一个对象,通过此对象调用AQS的方法,实现线程阻塞和唤醒
*/
private final Sync sync;
/**
* 只有一个构造方法,接收一个count值
*/
public CountDownLatch(int count) {
// count值不能小于0
if (count < 0) throw new IllegalArgumentException("count < 0");
// 直接创建一个Sync对象,并传入count值,Sync内部将会执行setState(count)
this.sync = new Sync(count);
}
2.5 await方法分析
CountDownLatch
类最最核心的两个方法就是await
以及ountDown
,我们先来看一看await
方法的实现:
// 此方法用来让当前线程阻塞,直到count减小为0才恢复执行
public void await() throws InterruptedException {
// 这里直接调用sync的acquireSharedInterruptibly方法,这个方法定义在AQS中
// 方法的作用是尝试获取共享锁,若获取失败,则线程将会被加入到AQS的同步队列中等待
// 直到获取成功为止。且这个方法是会响应中断的,线程在阻塞的过程中,若被其他线程中断,
// 则此方法会通过抛出异常的方式结束等待。
sync.acquireSharedInterruptibly(1);
}
await
的实现异常简单,只有短短一行代码,调用了AQS
中已经封装好的方法。这就是AQS
的好处,AQS
已经实现了线程的阻塞和唤醒机制,将实现的复杂性隐藏,而其他类只需要简单的使用它即可。为了方便理解,我们还是来看看acquireSharedInterruptibly
方法吧:
/** 此方法是AQS中提供的一个模板方法,用以获取共享锁,并且会响应中断 */
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 首先判断当前线程释放被中断,若被中断,则直接抛出异常结束
if (Thread.interrupted())
throw new InterruptedException();
// 调用tryAcquireShared方法尝试获取锁,这个方法被Sycn类重写了,
// 若count == 0,则这个方法会返回1,表示获取锁成功,则这里会直接返回,线程不会被阻塞
// 若count < 0,将会执行下面的doAcquireSharedInterruptibly方法,
// 此处请去查看Sync中tryAcquireShared方法的实现
if (tryAcquireShared(arg) < 0)
// 下面这个方法的作用是,线程获取锁失败,将会加入到AQS的同步队列中阻塞等待,
// 直到成功获取到锁,而此处成功获取到锁的条件就是count == 0,若当前线程在等待的过程中,
// 成功地获取了锁,则它会继续唤醒在它后面等待的线程,也尝试获取锁,
// 这也就是说,只要count == 0了,则所有被阻塞的线程都能恢复运行
doAcquireSharedInterruptibly(arg);
}
相信看到这里,对CountDownLatch
的实现原理已经有一个比较清晰的理解了。CountDownLatch
的实现完全就是依赖于AQS
的,所有再次提醒,如果以上内容理解不了,请先去学习AQS
。
2.6 countDown方法分析
下面我们来分析CountDownLatch
中另一个核心的方法——countDown
,
/**
* 此方法的作用就是将count的值-1,如果count等于0了,就唤醒等待的线程
*/
public void countDown() {
// 这里直接调用sync的releaseShared方法,这个方法的实现在AQS中,也是AQS提供的模板方法,
// 这个方法的作用是当前线程释放锁,若释放失败,返回false,若释放成功,则返回false,
// 若锁被释放成功,则当前线程会唤醒AQS同步队列中第一个被阻塞的线程,让他尝试获取锁
// 对于CountDownLatch来说,释放锁实际上就是让count - 1,只有当count被减小为0,
// 锁才是真正被释放,线程才能继续向下运行
sync.releaseShared(1);
}
为了方便理解,我们还是来看一看AQS
中releaseShared
方法的实现:
public final boolean releaseShared(int arg) {
// 调用tryReleaseShared尝试释放锁,这个方法已经由Sycn重写,请回顾上面对此方法的分析
// 若tryReleaseShared返回true,表示count经过这次释放后,等于0了,于是执行doReleaseShared
if (tryReleaseShared(arg)) {
// 这个方法的作用是唤醒AQS的同步队列中,正在等待的第一个线程
// 而我们分析acquireSharedInterruptibly方法时已经说过,
// 若一个线程被唤醒,检测到count == 0,会继续唤醒下一个等待的线程
// 也就是说,这个方法的作用是,在count == 0时,唤醒所有等待的线程
doReleaseShared();
return true;
}
return false;
}
三、总结
如果直接去看CountDownLatch
的源码会发现,它的实现真的非常简单,包括注释在内,总共300
行代码,除去注释,连100
行代码都不到。因为它所作的工作,除了重写AQS
的两个方法外,其余的基本上就是调用AQS
提供的模板方法而已。所以,理解CountDownLatch
的过程,实际上是理解AQS
的过程,只要理解了AQS
,看懂CountDownLatch
的原理,不需要5
分钟。AQS
真的是Java
并发中非常重要的一个组件,很多类都是基于它实现的,比如还有ReentrantLock
,同时AQS
也是面试中的常考点,所以一定要好好研究。最后再次推荐我之前编写的有关AQS
的源码分析博客:并发——抽象队列同步器AQS的实现原理。
四、参考
- JDK1.8源码
并发——深入分析CountDownLatch的实现原理的更多相关文章
- Java并发—–深入分析synchronized的实现原理
记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线 ...
- 并发——深入分析ThreadLocal的实现原理
一.前言 这篇博客来分析一下ThreadLocal的实现原理以及常见问题,由于现在时间比较晚了,我就不废话了,直接进入正题. 二.正文 2.1 ThreadLocal是什么 在讲实现原理之前, ...
- 聊聊并发(一)深入分析Volatile的实现原理
本文属于作者原创,原文发表于InfoQ:http://www.infoq.com/cn/articles/ftf-java-volatile 引言 在多线程并发编程中synchronized和Vola ...
- 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理
1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...
- (转载)java高并发:CAS无锁原理及广泛应用
java高并发:CAS无锁原理及广泛应用 版权声明:本文为博主原创文章,未经博主允许不得转载,转载请注明出处. 博主博客地址是 http://blog.csdn.net/liubenlong007 ...
- Java并发机制的底层实现原理之volatile应用,初学者误看!
volatile的介绍: Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现 ...
- Java 线程同步组件 CountDownLatch 与 CyclicBarrier 原理分析
1.简介 在分析完AbstractQueuedSynchronizer(以下简称 AQS)和ReentrantLock的原理后,本文将分析 java.util.concurrent 包下的两个线程同步 ...
- 《Java并发编程的艺术》Java并发机制的底层实现原理(二)
Java并发机制的底层实现原理 1.volatile volatile相当于轻量级的synchronized,在并发编程中保证数据的可见性,使用 valotile 修饰的变量,其内存模型会增加一个 L ...
- Java并发编程笔记之ConcurrentHashMap原理探究
在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap. HashTable是一个线程安全的类 ...
随机推荐
- 浅谈CSRF(跨站请求伪造)攻击方式
一.CSRF是什么? CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSR ...
- Spring扩展:替换IOC容器中的Bean组件 -- @Replace注解
1.背景: 工作中是否有这样的场景?一个软件系统会同时有多个不同版本部署,比如我现在做的IM系统,同时又作为公司的技术输出给其他银行,不同的银行有自己的业务实现(比如登陆验证.用户信息查询等) ...
- ssm框架整合---maven工具来导入jar包
ssm整合 一.导包(pom.xml文件) <?xml version="1.0" encoding="UTF-8"?> <project x ...
- Swift中的感叹号( ! )与问号( ? )之谜
基本了解 在Swift代码会经常看到定义属性或方法参数时类型后面会紧跟一个感叹号( ! )或问号( ? ), 刚开始接触Swift的童鞋就可能不太明白之代表什么意思,一头雾水,开始凌乱了. 本文将带你 ...
- golang超级mapper包 - coven
coven介绍 你可以把它理解成.NET 的 AutoMapper,java的modelmapper 一个快速的转换器去,支持结构到结构,切片到切片和映射到映射非反射转换,类型与嵌套指针支持. 不支持 ...
- 【Deep Learning读书笔记】深度学习中的概率论
本文首发自公众号:RAIS,期待你的关注. 前言 本系列文章为 <Deep Learning> 读书笔记,可以参看原书一起阅读,效果更佳. 概率论 机器学习中,往往需要大量处理不确定量,或 ...
- 支付宝小程序获取 user_id(openid) ThinkPHP版
支付宝小程序获取 user_id(openid) ThinkPHP版 近期支付宝小程序个人公测了,就想着玩一下,没想到就获取用户唯一标识都这么麻烦,微信的openid的话Get请求一下就完事了,支付宝 ...
- hdu1045 炮台的配置 dfs
只要炮台在同一行或者同一列,就可以互相摧毁,遇到墙则无法对墙后的炮台造成伤害,可以通过dfs搜索n*n的方格,全部搜完算一轮,计算炮台数,并保存其最大值. 其中对于t编号的炮台,位置可以计算出是(t/ ...
- 接口自动化框架pyface详细介绍
版权说明 本框架系本人结合一些实践经验和开源框架设计思想,在家基于兴趣爱好独立完成的代码开发. 源码只保存在私人电脑,办公电脑上无.github开源与公司无关,先把关系撇清,不涉及侵权. 嘘. 框架定 ...
- 【2019多校第一场补题 / HDU6582】2019多校第一场E题1005Path——最短路径+网络流
HDU6582链接 题意 在一张有向图中,有一个起点和一个终点,你需要删去部分路径,使得起点到终点的最短距离增加(并不要求需要使得距离变成最大值),且删除的路径长度最短.求删去的路径总长为多少 分析 ...