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共享排队的 ...
随机推荐
- Java 网络编程 -- 基于TCP实现文件上传
Java TCP 操作基本流程 一.创建服务器 1.指定端口, 使用serverSocket创建服务器 2.阻塞式连接 accept 3.操作:输入流 输出流 4.释放资源 二.创建客户端 1.使用S ...
- 反转链表-PHP的实现
<? //节点 class Node { private $Data;//节点数据 private $Next;//下一节点 public function setData($value) { ...
- 算法笔记刷题2(codeup 1928)
又磕了一晚上,多点测试真的很烦 ,完全不知道错哪里,后来发现是我变量名命名不规范导致自己晕了填错了,其实思路还是对的 我觉得书上的做法也还行,但我不太喜欢用二维数组,所以拿以前写的算天数的程序改装了一 ...
- bootstrap4中使用fontawesome5.6.3
先下载fontawesome5.6.3,选择free for web,下载完解压,丢在资源目录下 <form action=""> <div class=&quo ...
- zoj_2511 Design T-Shirt 贪心
Design T-Shirt Time Limit: 2 Seconds Memory Limit: 32768 KB Soon after he decided to design a T ...
- 【JAVA基础】08 面向对象3
1. 多态 多态polymorhic概述 事物存在的多种形态. 多态前提 要有继承关系 要有方法重写 要有父类引用指向子类对象 案例演示 代码体现多态 class Demo1_Polymorphic{ ...
- 【Linux网络基础】TCP/IP协议簇的详细介绍(三次握手四次断开,11种状态)
一.TCP/IP协议簇(DoD参考模型) 用于简化OSI层次,以及相关的标准. 传输控制协议(tcp/ip)簇是相关国防部DoD所创建的,主要用来确保数据的完整性以及在毁灭性战争中维持通信 是由一组不 ...
- mac OS 卸载node.js及npm
通过homebrew安装的 输入卸载命令 brew uninstall node 通过官网下载pkg安装包的 输入卸载命令 sudo rm -rf /usr/local/{bin/{node,npm} ...
- $_server[]关于浏览器和服务器的参数获取
$_SERVER['USER'] www $_SERVER['HOME'] /home/www $_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS'] 1 $_SERVE ...
- 基于 react 的Java web 应用—— 组件复用(后续需更新)
前言 实习第二周,被告知要用React与导师进行基于React的Javaweb 的开发,jinzhangaaaaa~由于React 这款框架没学过,看了一峰老师的基础入门教程,硬着头皮开始上了... ...