科普文:JUC系列之多线程门闩同步器CountDownLatch的使用和源码
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他10个线程的任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为那10个线程的数量也就是10。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上await()等待的线程就可以恢复执行任务。
CountDownLatch使用示例
CountDownLatch,一个同步辅助类,在完成特定的操作之前,它让一个或多个线程一直等待,
构造函数:new CountDownLatch(2);初始化门闩的长度为2;
它有两个主要方法:latch.await(),当前线程等待,直到门闩的值为0,线程才往下执行;
latch.countDown(),门闩值减一;
使用await在主线程阻塞,每个子线程执行完了,就调用latch.countDown()一次,知道最后门闩为0,解开主线程的等待;
比如主线程开启多个子线程,当所有子线程都执行完了,主线程才能继续往下执行,如下:
publicclassTest {publicstaticvoidmain(String[] args) {finalCountDownLatch latch =newCountDownLatch(2);newThread(){publicvoidrun() {try{System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");latch.countDown();}catch(InterruptedException e) {e.printStackTrace();}};}.start();newThread(){publicvoidrun() {try{System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");latch.countDown();}catch(InterruptedException e) {e.printStackTrace();}};}.start();try{System.out.println("等待2个子线程执行完毕...");latch.await();System.out.println("2个子线程已经执行完毕");System.out.println("继续执行主线程");}catch(InterruptedException e) {e.printStackTrace();}}}
CountDownLatch源码解读
下面我们来详细分析
构造函数:
public CountDownLatch(int count) { }; //参数count为计数值,也就是需要等几个线程结束的个数
它有三个主要方法:
public void await() throws InterruptedException { };
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
public void countDown() { };
await():调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
await(long timeout, TimeUnit unit):和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
countDown() :将count值减1,通常在等待的线程完成时调用,当10个线程都执行完,都减1后,count值为0,被挂起的线程就可以启动了。
实例:使用await在主线程阻塞,当每个子线程执行完了,就调用latch.countDown()一次,知道最后count的值为0,才解开主线程的等待;
public static void main(String[] args) {final CountDownLatch latch = new CountDownLatch(2);new Thread() {public void run() {System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");latch.countDown();};}.start();new Thread() {public void run() {System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");latch.countDown();};}.start();try {System.out.println("等待2个子线程执行完毕...");latch.await();System.out.println("2个子线程已经执行完毕");System.out.println("继续执行主线程");} catch (InterruptedException e) {e.printStackTrace();}}
输出结果:
子线程Thread-0正在执行
子线程Thread-0执行完毕
等待2个子线程执行完毕...
子线程Thread-1正在执行
子线程Thread-1执行完毕
2个子线程已经执行完毕
继续执行主线程
从输出上我们可以知道这个CountDownLatch的使用方法和执行过程了,接下来我们通过对它的主要方法的分析来看一下实现原理。
源码解析:
1.CountDownLatch(int count)
这个是CountDownLatch的构造函数,我们跟进看一下
public CountDownLatch(int count) {if (count < 0) throw new IllegalArgumentException("count < 0");this.sync = new Sync(count);}
先判断是否count的值是否正常,如果小于0,直接抛出异常,否则创建一个Sync对象
private static final class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 4982264981922014374L;Sync(int count) {setState(count);}int getCount() {return getState();}protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}protected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {int c = getState();if (c == 0)return false;int nextc = c-1;if (compareAndSetState(c, nextc))return nextc == 0;}}}
我们看到这里使用了AQS的Sync,唯一与lock不同的是把state设置为了count,然后获取锁的逻辑tryAcquireShared方法也做了对应的调整,这里获取锁的话判断state是否为0。
2.await()
await方法用于阻塞线程,也就是令当前线程阻塞直到拿到锁(state==0也就是count==0)
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);}
先判断是否中断,如果中断的话响应中断并抛出异常,结束阻塞,然后通过tryAcquireShared获取锁,我们来看tryAcquireShared方法
protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}
拿到锁的唯一条件就是state==0,也就是子线程通过countDown()方法把count变为0才可以。我们接下来先看doAcquireSharedInterruptibly()上锁的过程。
3.doAcquireSharedInterruptibly()
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 GCfailed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}
这里的逻辑和lock方法的逻辑基本一致,只是稍作修改,主线程通过parkAndCheckInterrupt方法进行了阻塞。
4.countDown()
public void countDown() {sync.releaseShared(1);}
public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}
先调用了tryReleaseShared来解锁,我们看一下这个过程
protected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {int c = getState();if (c == 0)return false;int nextc = c-1;if (compareAndSetState(c, nextc))return nextc == 0;}}
拿到state,然后通过CAS把state-1,最后返回Boolean值,如果state==0,说明锁释放返回true,如果state>0,返回false,这里也就证明了我们的猜想,countdown方法就是来把state每次减一的,直到所有子线程执行完,减为0,锁释放。我们继续看锁释放后的执行过程。
5.doReleaseShared()
private void doReleaseShared() {/** Ensure that a release propagates, even if there are other* in-progress acquires/releases. This proceeds in the usual* way of trying to unparkSuccessor of head if it needs* signal. But if it does not, status is set to PROPAGATE to* ensure that upon release, propagation continues.* Additionally, we must loop in case a new node is added* while we are doing this. Also, unlike other uses of* unparkSuccessor, we need to know if CAS to reset status* fails, if so rechecking.*/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 casesunparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue; // loop on failed CAS}if (h == head) // loop if head changedbreak;}}
这里的步骤也和lock的释放过程类似,最后通过waitStatus的判断来执行unparkSuccessor()唤醒阻塞的线程。
6.setHeadAndPropagate()
当唤醒await的线程后,会执行第3步doAcquireSharedInterruptibly()里的setHeadAndPropagate()
private void setHeadAndPropagate(Node node, int propagate) {Node h = head; // Record old head for check belowsetHead(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();}}
可以看到,把唤醒的node设置为了head节点,也就是node拿到锁可以继续执行了,那么如果有其它的await也在等待呢?此时count为0,其它的肯定也要向下执行,是怎么连续唤醒的呢,我们看本方法里的Node s = node.next;
这里判断后续阻塞节点,如果存在,就执行 doReleaseShared();持续唤醒,doReleaseShared()在也就是第5步,他会解除head节点的next的阻塞,然后再执行本步骤,设置为head,循环唤醒。
最后:
这里循环唤醒也是共享锁的实现方式。在这里我们也再次印证了AQS是java.util.concurrent包下几乎所有类的实现核心,像CountDownLatch、CyclicBarrier和Semaphore三大辅助类,lock等都是基于AQS来实现自己的控制逻辑的。
相关文章:
科普文:JUC系列之多线程门闩同步器CountDownLatch的使用和源码
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他10个线程的任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。 CountDownLatch是通过一个计数器来实现…...
foreach循环和for循环在PHP中各有什么优势
在PHP中,foreach循环和for循环都是用来遍历数组的常用结构,但它们各有其优势和使用场景。 foreach循环的优势 简化代码:foreach循环提供了一种更简洁的方式来遍历数组,不需要手动控制索引或指针。易于阅读:对于简单的…...

巧用casaos共享挂载自己的外接硬盘为局域网共享
最近入手了个魔改机顶盒,已经刷好了的armbian,虽然是原生的,但是我觉得挺强大的,内置了很多 常用的docker和应用,只需要armbian-software 安装就行,缺点就是emmc太小了。 买到之后第一时间装上了casaos和1p…...
标题:解码“八股文”:助力、阻力,还是空谈?
标题:解码“八股文”:助力、阻力,还是空谈? 在程序员的面试与职场发展中,“八股文”一直是一个备受争议的话题。它既是求职者展示自己技术功底的途径,也是一些公司筛选人才的标准之一。但“八股文”在实际…...

语言无界,沟通无限:2024年好用在线翻译工具推荐
随着技术的发展现在的翻译在线工具从基础词句翻译到复杂的文章翻译都不在话下。为了防止你被五花八门的工具挑花眼,我给你介绍几款我用过的便捷、高效、准确的翻译工具吧。 1.福晰翻译端 链接直通:https://www.foxitsoftware.cn/fanyi/ 这个软件支持…...

【Golang 面试 - 进阶题】每日 3 题(十八)
✍个人博客:Pandaconda-CSDN博客 📣专栏地址:http://t.csdnimg.cn/UWz06 📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话,欢迎点赞👍收藏…...

二分+dp,CF 1993D - Med-imize
一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 D - Med-imize 二、解题报告 1、思路分析 对于n < k的情况直接排序就行 对于n > k的情况 最终的序列长度一定是 (n - 1) % k 1 这个序列是原数组的一个子序列 对于该序列的第一个元素࿰…...

三十种未授权访问漏洞复现 合集( 三)
未授权访问漏洞介绍 未授权访问可以理解为需要安全配置或权限认证的地址、授权页面存在缺陷,导致其他用户可以直接访问,从而引发重要权限可被操作、数据库、网站目录等敏感信息泄露。---->目录遍历 目前主要存在未授权访问漏洞的有:NFS服务&a…...

数据湖和数据仓库核心概念与对比
随着近几年数据湖概念的兴起,业界对于数据仓库和数据湖的对比甚至争论就一直不断。有人说数据湖是下一代大数据平台,各大云厂商也在纷纷的提出自己的数据湖解决方案,一些云数仓产品也增加了和数据湖联动的特性。但是数据仓库和数据湖的区别到…...
探索WebKit的奥秘:打造高效、兼容的现代网页应用
1. 简介 1.1. 主要特点 WebKit 是一个开源的浏览器引擎,它允许开发者构建高性能、功能丰富的 web 应用程序。WebKit 与 Mozilla Firefox 等使用的 Gecko 引擎、Internet Explorer 使用的 Trident 引擎以及 EdgeHTML 引擎共同构成了现代 web 浏览器的核心技术。 1.2. 学习资…...

【leetcode】平衡二叉树、对称二叉树、二叉树的层序遍历(广度优先遍历)(详解)
Hi~!这里是奋斗的明志,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 🌱🌱个人主页:奋斗的明志 🌱🌱所属专栏:数据结构、LeetCode专栏 📚本系…...
最短路径算法:Floyd-Warshall算法
引言 在图论中,Floyd-Warshall算法是一种用于计算任意两点之间最短路径的动态规划算法。它适用于加权有向图和无向图,可以处理带有负权重边的图,但要求图中不能有负权重环。本文将详细介绍Floyd-Warshall算法的定义、步骤及其实现。 Floyd-…...

3DM游戏运行库合集离线安装包2024最新版
3DM游戏运行库合集离线安装包是一款由国内最大的游戏玩家论坛社区3DM推出的集成式游戏运行库合集软件,旨在解决玩家在玩游戏时遇到的运行库缺失或错误问题。该软件包含多种常用的系统运行库组件,支持32位和64位操作系统,能够自动识别系统版本…...
【Bigdata】什么是混合型联机分析处理
这是我父亲 日记里的文字 这是他的生命 留下留下来的散文诗 几十年后 我看着泪流不止 可我的父亲已经 老得像一个影子 🎵 许飞《父亲写的散文诗》 混合型联机分析处理(Hybrid OLAP,简称 HOLAP)是一种结合了多…...
Java 并发编程:volatile 关键字介绍与使用
大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 026 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进…...

【Spark计算引擎----第三篇(RDD)---《深入理解 RDD:依赖、Spark 流程、Shuffle 与缓存》】
前言: 💞💞大家好,我是书生♡,本阶段和大家一起分享和探索大数据技术Spark—RDD,本篇文章主要讲述了:RDD的依赖、Spark 流程、Shuffle 与缓存等等。欢迎大家一起探索讨论!࿰…...
四、日志收集loki+ promtail+grafana
一、简介 Loki是受Prometheus启发由Grafana Labs团队开源的水平可扩展,高度可用的多租户日志聚合系统。 开发语言: Google Go。它的设计具有很高的成本效益,并且易于操作。使用标签来作为索引,而不是对全文进行检索,也就是说&…...

xdma的linux驱动编译给arm使用(中断检测-测试程序)
1、驱动链接 XDMA驱动源码官网下载地址为:https://github.com/Xilinx/dma_ip_drivers 下载最新版本的XDMA驱动源码,即master版本,否则其驱动用不了(xdma ip核版本为4.1)。 2、驱动 此部分来源于博客:xd…...
探索之路——初识 Vue Router:构建单页面应用的完整指南
目录 1. Vue Router 简介 2. 安装与配置 Vue Router 安装步骤 配置路由 3. 在 Vue 应用中使用路由 4. 进阶使用 路由守卫 懒加载 高级路由技术 嵌套路由 动态路由匹配 编程式的路由导航 路由懒加载 路由元信息 在现代前端开发中,单页面应用(SPA)因其出…...

传输层_计算机网络
文章目录 运输层UDPTCPTCP连接管理TCP三次握手TCP四次挥手 可靠机制流量控制拥塞控制 QUIC 运输层 网络层提供了主机之间的逻辑通信 运输层为运行在不同主机上的进程之间提供了逻辑通信 UDP(用户数据报协议)提供一种不可靠、无连接的服务,数据报 TCP(传输控制协议)…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...

【单片机期末】单片机系统设计
主要内容:系统状态机,系统时基,系统需求分析,系统构建,系统状态流图 一、题目要求 二、绘制系统状态流图 题目:根据上述描述绘制系统状态流图,注明状态转移条件及方向。 三、利用定时器产生时…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...
LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》
这段 Python 代码是一个完整的 知识库数据库操作模块,用于对本地知识库系统中的知识库进行增删改查(CRUD)操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 📘 一、整体功能概述 该模块…...

【LeetCode】算法详解#6 ---除自身以外数组的乘积
1.题目介绍 给定一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O…...
js 设置3秒后执行
如何在JavaScript中延迟3秒执行操作 在JavaScript中,要设置一个操作在指定延迟后(例如3秒)执行,可以使用 setTimeout 函数。setTimeout 是JavaScript的核心计时器方法,它接受两个参数: 要执行的函数&…...