JUC之CountDownLatch源码分析
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源码分析的更多相关文章
- Java - "JUC" CountDownLatch源码分析
Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例 CountDownLatch简介 CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前 ...
- CountDownLatch 源码分析
CountDownLatch 源码分析: 1:CountDownLatch数据结构 成员变量 Sync类型对象 private final Sync sync; Sync是继承AQS的一个类,Coun ...
- concurrent(五)同步辅助器CountDownLatch & 源码分析
参考文档: https://blog.csdn.net/zxdfc/article/details/52752803 简介 CountDownLatch是一个同步辅助类.允许一个或多个线程等待其他线程 ...
- JUC AQS ReentrantLock源码分析
警告⚠️:本文耗时很长,先做好心理准备,建议PC端浏览器浏览效果更佳. Java的内置锁一直都是备受争议的,在JDK1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6 ...
- 并发工具CountDownLatch源码分析
CountDownLatch的作用类似于Thread.join()方法,但比join()更加灵活.它可以等待多个线程(取决于实例化时声明的数量)都达到预期状态或者完成工作以后,通知其他正在等待的线程继 ...
- JUC之ReentrantLock源码分析
ReentrantLock:实现了Lock接口,是一个可重入锁,并且支持线程公平竞争和非公平竞争两种模式,默认情况下是非公平模式.ReentrantLock算是synchronized的补充和替代方案 ...
- java源码-CountDownLatch源码分析
这次分析CountDownLatch,相信大部分人都用过把! CountDownLatch内部还是Sync对象,还是基础AQS(可见其重要性),首先看一下CountDownLatch初始化,Count ...
- Java并发系列[7]----CountDownLatch源码分析
CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行.它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须 ...
- CountDownLatch源码分析
CountDownLatch.Semaphore(信号量)和ReentrantReadWriteLock.ReadLock(读锁)都采用AbstractOwnableSynchronizer共享排队的 ...
随机推荐
- 一站式轻量级框架 Spring
Spring 简介 Spring 是一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的.Spring 的核心是控制反转(IoC)和面向切面编程(AOP).简单来说,Spring ...
- coding++:@DisallowConcurrentExecution 注解的作用
Quartz定时任务默认都是并发执行的,不会等待上一次任务执行完毕,只要间隔时间到就会执行, 如果定时任执行太长,会长时间占用资源,导致其它任务堵塞. 在Spring中这时需要设置concurrent ...
- 曹工杂谈--只用一个命令,centos系统里装了啥软件,啥时候装的,全都清清楚楚
前言 一直以来,对linux的掌握就是半桶水的状态,经常yum装个东西,结果依赖一堆东西:然后再用源码装个东西,只知道make.make install,背后干了啥也不清楚了,卸载也不方便. 这几天工 ...
- 列表按钮功能的设置和DOM的使用
HTML: <foreach name="fulltime_list" item="v"> <tr> <td></td ...
- [Windows API] Listing the Files in a Directory,可用来数文件夹下有多少个子文件(夹)
转载 #include <windows.h> #include <tchar.h> #include <stdio.h> #include <strsafe ...
- vue与众不同的学习方式,让她年薪200多万
学习vue正确思路,是先学vue-cli,再学vue.js单文件引用的用法,这样会在极短时间内撤底撑握vue,如果先学vue.js单文件用法,再去学vue-cli4,可以说是重新学vue,,,,难处大 ...
- 【java基础】01 计算机基础知识
一.计算机基础知识 1. 计算机 1. 什么是计算机? 计算机在生活中的应用举例 计算机(Computer)全称:电子计算机,俗称电脑.是一种能够按照程序运行,自动.高速处理海量数据的现代化智能电子设 ...
- Spring IOC 之注册解析的 BeanDefinition
2019独角兽企业重金招聘Python工程师标准>>> DefaultBeanDefinitionDocumentReader.processBeanDefinition() 完成 ...
- 以内存级速度实现存储?XPoint正是我们的计划
随着计算能力虚拟化技术的普及,存储机制在速度上远逊于内存这一劣势开始变得愈发凸显. 这一巨大的访问速度鸿沟一直是各项存储技术想要解决的核心难题:纸带.磁带.磁盘驱动器乃至闪存记忆体等等,而如今最新一代 ...
- Mysql 开窗函数实战
Mysql 开窗函数实战 Mysql 开窗函数在Mysql8.0+ 中可以得以使用,实在且好用. row number() over rank() over dense rank() ntile() ...