JUC包:CountDownLatch源码+实例讲解
1 缘起
有一次听到同事谈及AQS时,我有很多点懵,
只知道入队和出队,CLH(Craig,Landin and Hagersten)锁,并不了解AQS的应用,
同时结合之前遇到的多线程等待应用场景,发现CountDownLatch是使用AQS实现的线程等待,
本文即通过实战和源码分析,探究CountDownLatch是如何利用AQS实现线程等待的,
帮助读者轻松应对知识交流与考核。
2 CountDownLatch
同步辅助工具,允许一个或多个线程等待其他线程操作完成。
CountDownLatch通过给定同步状态数量初始化,即state数量(参见AQS,AbstractQueuedSynchronizer)。
执行过程中,会通过调用countDown方法来阻塞await方法,直到同步状态state为0时,释放所有阻塞的线程,立即返回结果。
CountDownLatch的同步状态数量是一次性的操作,无法重置次数。
如果需要多次使用计数,可以使用CyclicBarrier。
CountDownLatch是通用的同步工具,用途广泛。
CountDownLatch初始化同步状态数量为1,可以作为一个简单的开/关锁存器或门:所有调用await方法的线程都在门处等待,直到线程调用countDown方法开启门。
CountDownLatch初始化同步状态数量为N,线程等待N个线程完成操作或某个线程完成N次。
CountDownLatch非常有用的一个特性是:执行前不要求调用countDown的线程等待同步状态数量达到0,仅仅是通过await方法阻止线程通过,直到所有任务都完成,一次性通过所有线程。
内存一致性影响:同步状态数量达到0之前,先调用countDown()的线程执行优先于其他线程调用await获取结果。
2.1 测试样例
CountDownLatch两个核心方法:countDown和await,
其中,
countDown同步状态数量减1,CountDownLatch继承AbstractQueuedSynchronizer(AQS),通过state属性检测是否获取或释放当前资源,所以,countDown方法主要的任务就是减少state值,释放资源。
await则是阻塞线程,state未达到0时持续自旋,等待,最终所有线程任务执行完成后立即返回,继续执行后面的逻辑。
下面给出具体的测试样例,新建CountDownLatch,初始化同步状态数state=3,执行三次countDown,释放线程,继续执行。
package com.monkey.java_study.juc;import org.apache.commons.lang3.time.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.Random;
import java.util.concurrent.CountDownLatch;/*** CountDownLatch测试.** @author xindaqi* @since 2023-02-20 15:00*/
public class CountDownLatchTest {private static final Logger logger = LoggerFactory.getLogger(CountDownLatchTest.class);/*** 线程执行器,* 执行countDown,计数器减一*/static class ThreadRunner implements Runnable {private final CountDownLatch countDownLatch;public ThreadRunner(CountDownLatch countDownLatch) {this.countDownLatch = countDownLatch;}@Overridepublic void run() {try {StopWatch stopWatch = new StopWatch();stopWatch.start();Random random = new Random();int randomBound = 1000;Thread.sleep(random.nextInt(randomBound));stopWatch.stop();logger.info(">>>>>>>>{}, time cost:{}", Thread.currentThread().getName(), stopWatch.formatTime());countDownLatch.countDown();} catch (Exception ex) {throw new RuntimeException(ex);}}}public static void main(String[] args) {StopWatch stopWatch = new StopWatch();stopWatch.start();int threadCount = 3;CountDownLatch countDownLatch = new CountDownLatch(threadCount);for (int i = 0; i < 3; i++) {new Thread(new ThreadRunner(countDownLatch)).start();}try {countDownLatch.await();stopWatch.stop();logger.info(">>>>>>>>Time cost:{}", stopWatch.formatTime());} catch (Exception ex) {throw new RuntimeException(ex);}}
}
测试结果如下图所示,
由结果可知,三个线程各自执行,通过不同的延迟时间模拟程序执行的时间,
执行耗时最长的Thread-2执行结束后,释放线程,继续执行后续的逻辑。

3 源码分析
下面根据源码分析CountDownLatch是如何实现线程等待以及释放资源的。
3.1 初始化
首先从初始化CountDownLatch开始讲解,源码如下图所示,
由源码可知,通过count参数初始化AQS的同步状态数state,
Sync即CountDownLatch中继承AbstractQueuedSynchronizer的内部类,
通过setState(count)初始化state,这个放在Sync中讲,先提示一下。
位置:java.util.concurrent.CountDownLatch#CountDownLatch

3.2 Sync
下面进入Sync源码,如下图所示,
由图可知,Sync是CountDownLatch的内部私有静态类,继承AbstractQueuedSynchronizer类,
构造函数参数为count,用于初始化state,即setState(count),
通过getCount()调用getState方法获取同步状态数量。
同时重写了tryAcquireShared和tryReleaseShared两个方法,
用于获取共享资源和释放共享资源。
位置:java.util.concurrent.CountDownLatch.Sync

获取共享资源和释放共享资源的源码如下图所示,
由图可知,tryAcquireShared通过获取同步状态数量标识当前资源状态,
同步状态不为0时,返回-1,为0时返回1,为后续提供操作标识,后文会反复用到,到时详解。

3.3 countDown
同步状态减1通过countDown方法实现,源码如下图所示,
由图可知,该方法调用了releaseShared方法,实现状态减1,这里还看不出具体的逻辑,
接着往下看,进入方法releaseShared。
位置:java.util.concurrent.CountDownLatch#countDown

3.3.1 releaseShared
releaseShared源码如下图所示,由图可知,
判断条件中调用了tryReleaseShared方法,为true时才会调用doReleaseShared,否则,直接返回。
其中,tryReleasShared方法由前文可知,
Sync内部类继承AQS,重写了该方法,实现同步状态state减1,state=0返回fasle,否则返回true,
由此可知,releaseShared方法通过tryReleasShared实现状态减1,在state不为0时,不会调用doReleaseShared方法。
当state=0时,才会调用doReleaseShared方法,此时,所有线程已经完成任务,将要全部通过latch(门闩),
由上述可推测,doReleaseShared用于释放资源。
这个方法并没有实现自旋等待,而是在await方法中进行等待的,在state没有达到0时,不会让线程通过。
位置:java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared

tryReleaseShared源码标注如下图所示,
由图可知,通过自旋的方式使同步状态减1,state=0返回true,否则返回false,供后续方法判断。
位置:java.util.concurrent.CountDownLatch.Sync#tryReleaseShared

doReleaseShared则是释放资源,源码如下图所示,
由图可知,通过修改节点等待状态(waitStatus)为0,以及取消标记unparkSuccessor释放线程(这个放在后面讲),
通过自旋的方式,依次释放节点标记的线程资源,最终释放所有标记的资源。
位置:java.util.concurrent.locks.AbstractQueuedSynchronizer#doReleaseShared

unparkSuccessor释放节点标记的线程,源码如下图所示,
由源码可知,获取当前节点的后继节点,
后继节点不为空时,通过LockSupport.unpark释放线程标记,
当节点的后继节点不为空时,会释放当前节点标记的线程,即节点不再等待线程,直接释放,
实现最后一个线程执行结束后,取消节点对线程的占用,释放资源。
这里直接取消线程标记,也是导致同步状态state无法重用的直接原因,已经取消标记,后续自然无法使用,
同时,state置为0后,没有重新配置,所以依旧不能重用state。

3.4 await
综上分析,CountDownLatch控制线程等待的逻辑是在await中实现的,
因为,上面的逻辑只是实现了同步状态减1,以及释放线程标记等工作,没有阻塞线程,
所以,在await阻塞线程,当所有线程均完成各自逻辑时,即同步状态state=0,才开启“闸门”,释放锁。
await源码如下图所示,由图可知,await调用acquireSharedInterruptly方法,
当同步状态state=0时,立即返回。
位置:java.util.concurrent.CountDownLatch#await()

3.4.1 acquireSharedInterruptibly
线程等待。同步状态state不为0时,线程间相互等待。
acquireSharedInterruptibly源码如下图所示,
由图可知,判断条件通过tryAcquireShared获取,当state不为0时,返回-1,参见上文的tryAcquireShared方法,
调用doAcquireSharedInterrutly方法,实现线程等待。
位置:java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly

3.4.2 doAcquireSharedInterruptily
线程等待具体实现,源码如下图所示,
由图可知,每次调用都会向队列插入一个新的节点Node.SHARED,
然后自旋,即线程等待,当前节点的前驱节点为头节点时,获取同步状态state,
state不为0时,tryAcquireShared=-1,不会跳出自旋,
继续等待countDown操作,
当countDown使state减为0时,tryAcquireShared返回1,开始执行r>=0后续的逻辑,
跳出自旋,立即返回,即await立即返回,继续执行await后续的逻辑,释放线程等待。
位置:java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireSharedInterruptibly

上面提到获取当前节点的前驱节点,通过predecessor方法,
源码如下图所示,由图可知,返回节点的前驱节点。
位置:java.util.concurrent.locks.AbstractQueuedSynchronizer.Node#predecessor

至此完成CountDownLatch多线程同步状态递减以及相互等待的分析。
4 小结
(1)CountDownLatch是一次性的线程等待工具,实现多线程相互等待,直到所有线程均完成执行逻辑,用完即抛,无法重用;
(2)countDown方法使同步状态减1,当一个线程执行完成后,调用countDown,标识当前线程已完成,当state=0时,释放节点的线程占用,这也是CountDownLatch无法重用的直接原因,当然,没有重复保存state也是另一个原因;
(3)await是线程等待的核心,当同步状态state不为0时,会自旋等待,而不触发跳出自旋,当state=0时,才会立即返回,跳出自旋,结束线程等待;
(4)当某个线程出现异常时,这里需要格外注意,为保证程序正常中断,需要手动捕获异常,同时调用countDown方法,或者使用重试机制,否则线程不会退出,因为state最终的状态不为0,有一个线程线程而没有减1。
相关文章:
JUC包:CountDownLatch源码+实例讲解
1 缘起 有一次听到同事谈及AQS时,我有很多点懵, 只知道入队和出队,CLH(Craig,Landin and Hagersten)锁,并不了解AQS的应用, 同时结合之前遇到的多线程等待应用场景,发现…...
Log4j2基本使用
文章目录1. Log4j2入门2. Log4j2配置3. Log4j2异步日志4. Log4j2的性能Apache Log4j 2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带 来了一些重大的提升,主要有: 异常处理,…...
A2L在CAN FD总线的使用
文章目录 前言CAN时间参数BTL CyclesTime Quantum时间份额SWJ同步跳转宽度波特率计算采样点计算CAN FD的第二采样点SSP推荐配置A2L配置总结前言 A2L作为XCP标定协议的载体,包括了总线信息的定义。本文介绍如何将基于CAN总线的A2L扩展为支持CAN-FD的A2L CAN时间参数 在介绍配…...
Android JetPack之启动优化StartUp初始化组件的详解和使用
一、背景 先看一下Android系统架构图 在Android设备中,设备先通电(PowerManager),然后加载内核层,内核走完,开始检查硬件,以及为硬件提供的公开接口,然后进入到库的加载。库挂载后开…...
[11]云计算|简答题|案例分析|云交付|云部署|负载均衡器|时间戳
升级学校云系统我们学校要根据目前学生互联网在线学习、教师教学资源电子化、教学评价过程化精细化的需求,计划升级为云教学系统。请同学们根据学校发展实际考虑云交付模型包含哪些?云部署采用什么模型最合适?请具体说明。9月3日买电脑还是租…...
C++11/C++14:lambda表达式
概念 lambda表达式:是一种表达式,是源代码的组成部分闭包:是lambda表达式创建的运行期对象,根据不同的捕获模式,闭包会持有数据的副本或引用闭包类:用于实例化闭包的类,每个lambda表达式都会触…...
算法课堂-分治算法
分治算法 把一任务分成几部分(通常是两部分)来完成(或只完成一部分),从而实现整个任务的完成 或者你可以把递归理解为分治算法的一部分 因为递归就是把问题分解来解决问题 例子 称假币 最笨的方法:两两称…...
操作系统权限提升(十六)之绕过UAC提权-CVE-2019-1388 UAC提权
系列文章 操作系统权限提升(十二)之绕过UAC提权-Windows UAC概述 操作系统权限提升(十三)之绕过UAC提权-MSF和CS绕过UAC提权 操作系统权限提升(十四)之绕过UAC提权-基于白名单AutoElevate绕过UAC提权 操作系统权限提升(十五)之绕过UAC提权-基于白名单DLL劫持绕过UAC提权 注&a…...
实例9:四足机器人运动学正解平面RR单腿可视化
实例9:四足机器人正向运动学单腿可视化 实验目的 通过动手实践,搭建mini pupper四足机器人的腿部,掌握机器人单腿结构。通过理论学习,熟悉几何法、旋转矩阵法在运动学正解(FK)中的用处。通过编程实践&…...
堆的基本存储
一、概念及其介绍堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。堆满足下列性质:堆中某个节点的值总是不大于或不小于其父节点的值。堆总是一棵完全二叉树。二、适用说明堆是利用完全二叉树的结构来维护一组数…...
如何获取物体立体信息通过一个相机
大家都知道的3D 技术是通过双眼视觉差异 得到的 但是3D的深度并没有那么强 为什么眼睛看到的就那么强 这无法让我们相信这个视觉差理论是和人眼睛立体感是一个原理 这个如今3D 电影都在用的技术 是和真正的人眼立体感 不一样的 或者说是有瑕疵的 分析一下现在的立体感技术 是通…...
【数据挖掘实战】——中医证型的关联规则挖掘(Apriori算法)
目录 一、背景和挖掘目标 1、问题背景 2、传统方法的缺陷 3、原始数据情况 4、挖掘目标 二、分析方法和过程 1、初步分析 2、总体过程 第1步:数据获取 第2步:数据预处理 第3步:构建模型 三、思考和总结 项目地址:Data…...
一些硬件学习的注意事项与快捷方法
xilinx系列软件 系统适用版本 要安装在Ubuntu系统的话,要注意提前看好软件适用的版本,不要随便安好了Ubuntu系统又发现对应版本的xilinx软件不支持。 如下图,发行说明中会说明这个版本的软件所适配的系统版本。 下载 vivado vitis这些都可以…...
【Tomcat】Tomcat安装及环境配置
文章目录什么是Tomcat为什么我们需要用到Tomcattomcat下载及安装1、进入官网www.apache.org,找到Projects中的project List2、下载之后,解压3、找到tomcat目录下的startup.bat文件,双击之后最后结果出现多少多少秒,表示安装成功4、…...
负载均衡:LVS 笔记(二)
文章目录LVS 二层负载均衡机制LVS 三层负载均衡机制LVS 四层负载均衡机制LVS 调度算法轮叫调度(RR)加权轮叫调度(WRR)最小连接调度(LC)加权最小连接调度(WLC)基于局部性的最少链接调…...
SEO优化:干货技巧分享,包新站1-15天100%收录首页
不管是老域名还是新域名,不管是多久没有收录首页的站,此法周期7-30天,包首页收录!本人不喜欢空吹牛逼不实践的理论,公布具体操作:假如你想收录的域名是a.com,那么准备如下材料1.购买5-10个最便宜…...
JavaWeb测试题
【第四小组】【姓名:郑梦飞】说明:上方【组】填入所在的组,上方【姓名】填入自己的真实姓名。答题方式,基于Word文档基础上答题编程题可利用工具编程完以后,复制到该文档内。答完以后,导成PDF。以姓名.PDF命…...
Java EE|TCP/IP协议栈之数据链路层协议详解
文章目录一、数据链路层协议感性认识数据链路层简介以太网简介特点二、以太网数据帧格式详解帧头不同类型对应的载荷三、关于MTU什么是MTUMTU有什么作用ip分片(了解)参考一、数据链路层协议感性认识 数据链路层简介 从上图可以看出 , 在TCP/…...
Lighthouse组合Puppeteer检测页面
如上一篇文章lighthouse的介绍和基本使用方法结尾提到的一样,我们在实际使用Lighthouse检测页面性能时,通常需要一定的业务前置条件,比如最常见的登录操作、如果没有登录态就没有办法访问其他页面。再比如有一些页面是需要进行一系列的操作&a…...
【C++】仿函数、lambda表达式、包装器
1.仿函数 仿函数是什么?仿函数就是类中的成员函数,这个成员函数可以让对象模仿函数调用的行为。 函数调用的行为:函数名(函数参数)C中可以让类实现:函数名(函数参数)调用函数 自己写一个仿函数: 重载()运算符 cla…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...
Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
