线程等待其他线程执行同步类CountDownLatch

文章目录

    • 前言
    • 核心原理
    • 源码解析
      • 同步源码分析
      • await源码分析
      • countDown源码分析
    • 实战演示
      • 1、创建演示代码
      • 2、创建测试用例
      • 3、测试结果演示
    • 写在最后

前言

大家都知道多线程在我们实际编码过程中运用很多,很多情况我们需要靠多线程来提升系统性能。但是有些时候我们需要阻塞一部分线程,让这部分线程等待其他线程的执行完成获取结果。比如:数据统计、等待其他任务完成、死锁检查等等。为了应对这些场景,我们JUC提供了CountDownLatch类,这个类可以帮助我们在多线程的使用中让一个或多个线程等待其他线程执行完成。

核心原理

CountDownLatch内部维护了计数器和阻塞队列,计数器缓存count值,也是有多个需要先执行的线程数目;阻塞队列缓存的是调用了await()方法的线程,表示加入阻塞队列。当CountDownLatch初始化的时候会传入业务需要先执行的线程数目count值,后续线程调用await()方法则加入阻塞队列等待其他线程执行完成。countDown()方法则是在某个线程业务完成后将计数器count值减一,表示这个线程已经执行。如果所有先行线程执行完成,最终count值等于0,此时CountDownLatch就会唤醒阻塞队列中的线程继续执行。

源码解析

同步源码分析

CountDownLatch依赖关系图
在这里插入图片描述

进入JUC下CountDownLatch类,发现其内部类Sync集成AbstractQueuedSynchronizer抽象阻塞队列:

//内部类sync集成AQS
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 zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}

private final Sync sync;

继续查看源码:

//有参数的构造方法
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}


//线程加入阻塞队列方法
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}


//线程执行完成计数器减少方法
public void countDown() {
    sync.releaseShared(1);
}

如上源码所示,我们发现CountDownLatch的主要方法await()、countDown()、构造方法都是调用Syn内部类中的方法。然而Sync内部类继承了AQS,所以本质上我们CountDownLatch是通过AQS来实现同步的。

AQS原理的内部通过state状态和阻塞队列实现的,当 state == 0 唤醒阻塞队列中线程,对AQS还不是很了解的同学可以参考我的博客:并发基础之抽象同步队列AQS

await源码分析

上面我们分析了CountDownLatch同步是通过AQS实现的,现在我们详细分析下CountDownLatch的核心方法await()方法。

查询await()方法源码,发现CountDownLatch提供了两个让线程加入阻塞队列的方法,一个直接中断加入阻塞,另一个带有阻塞超时时间加入阻塞队列。

/**
 * 当前线程中断执行,加入阻塞队列
ublic void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

/**
 * 当前多线程中断加入阻塞队列,加入了阻塞超时时间
 */
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

继续查看源码:

//AQS执行中断加入阻塞前验证
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
//AQS执行中断加入阻塞队列
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);
    }
}

如上源码所示,CountDownLatch的await()方法本质是调用AQS中的阻塞方法。内部先验证阻塞队里是否有线程,如果没有则作为阻塞队列头部,倘若存在阻塞线程则加入到阻塞队列尾部。

countDown源码分析

上面分析了await()方法内部是调用AQS阻塞逻辑,不出意外countDown()方法应该也是调用的AQS处理释放线程锁的方法。

我们查询countDown源码:

//countDownLatch释放共享锁
public void countDown() {
    sync.releaseShared(1);
}
//AQS释放共享锁验证
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        //释放共享锁验证通过
        doReleaseShared();
        return true;
    }
    return false;
}

//countdownlatch内部类sync 覆写tryReleaseShared 方法
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))
            //只有state == 0 时候会释放共享锁
            return nextc == 0;
    }
}

如上源码所示,countDown()方法内部调用AQS的releaseShared()释放共享锁方法。在释放锁之前会验证是否满足释放条件,此时调用的是我们sync 覆写的tryReleaseShared()方法,这个方法逻辑是获取当前state值并用CAS修改state减一。然后判断state是否等于0,不等于0则不通过释放共享锁验证,等于0则通过验证。

我们继续查释放共享锁源:

//释放共享锁具体逻辑
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 cases
                //唤醒后续节点
                unparkSuccessor(h);
            }
            //用CAS重新设置state值
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
/**
 * 唤醒后续阻塞线程方法
 */
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //唤醒线程
        LockSupport.unpark(s.thread);
}

如上源码所示,doReleaseShared()方法释放了共享锁,释放完成发现阻塞队列有阻塞线程会唤醒线程。

实战演示

上面我们讲些了CountCownLatch的同步方式,以及主要方法的源码解析。相信大家对CountDownLatch都有了一定的了解,以下我们让主线程等待5个线程执行完成后再执行实战演示。

1、创建演示代码

/**
 * CountDownLatch运用
 * @author senfel
 * @version 1.0
 * @date 2023/4/20 14:58
 */
public class CountDownDemo {

    public static void actionCountDown() throws Exception{
        //创建一个定长线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //主线程等待5个线程执行完才执行
        CountDownLatch countDownLatch = new CountDownLatch(5);
        //并发量控制在2个线程
        Semaphore semaphore = new Semaphore(2);
        //创建线程执行数据
        for(int i=0;i<5;i++){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        System.err.println("线程:"+Thread.currentThread().getName()+"开始执行");
                        Thread.sleep(5000);
                        System.err.println("线程:"+Thread.currentThread().getName()+"执行完成");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                    semaphore.release();
                }
            });
        }
        countDownLatch.await();
        System.err.println("主线程执行");
    }
}

2、创建测试用例

@SpringBootTest
class DemoApplicationTests {

    /**
     * countDownLatchTest
     * @throws Exception
     */
    @Test
    public void countDownLatchTest() throws Exception{
        CountDownDemo.actionCountDown();
    }
}

3、测试结果演示

线程:pool-1-thread-1开始执行
线程:pool-1-thread-4开始执行
线程:pool-1-thread-1执行完成
线程:pool-1-thread-4执行完成
线程:pool-1-thread-3开始执行
线程:pool-1-thread-2开始执行
线程:pool-1-thread-3执行完成
线程:pool-1-thread-2执行完成
线程:pool-1-thread-5开始执行
线程:pool-1-thread-5执行完成
主线程执行

解释:
countDownLatch 保证主线程最后执行,其他子线程用semaphore 保证并发数目。

写在最后

本文我们讲述了CountDownLatch 是通过AQS抽象阻塞队列实现同步功能的,又说到了awat()方法本质是调用AQS加入阻塞队列的方法,countDown本质是调用AQS释放共享锁的方法。对于最很重要的唤醒阻塞线程逻辑,也是通过CountDownLatch 覆写tryReleaseShared()方法达到满足释放共享锁条件的,在AQS释放共享锁中如果阻塞队列存在阻塞线程会唤醒线程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/13586.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言开发环境搭建及调试

C简介 可移植 标准C C/C &#xff08;系统硬件操作的接口&#xff0c;windows&#xff0c;Linux不一样&#xff09; 跨平台 Java Python 下载 去官网选择Visual Studio 2019下载 安装过程中勾选使用C的桌面开发 安装好之后点击创建新项目——空项目 位置最好放在根目录下&…

【vue2】近期bug收集与整理02

⭐【前言】 在使用vue2构建页面时候&#xff0c;博主遇到的问题难点以及最终的解决方案。 &#x1f973;博主&#xff1a;初映CY的前说(前端领域) &#x1f918;本文核心&#xff1a;博主遇到的问题与解决思路 目录 ⭐数据枚举文件的使用⭐elementUI中分页组件使用的注意事项⭐…

OpenAI-ChatGPT最新官方接口《从0到1生产最佳实例》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(十一)(附源码)

Production Best Practices 生产最佳实例 前言Introduction 导言Setting up your organization 设置您的组织Managing billing limits 管理计费限额API keys API密钥Staging accounts 演示账户 Building your prototype 构建您的原型Additional tips 其它技巧 Techniques for i…

C++函数重载

目录 函数重载函数重载是怎样实现的 函数重载 函数重载&#xff1a;是函数的一种特殊情况&#xff0c;C允许在同一作用域中声明几个功能类似的同名函数&#xff0c;这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同&#xff0c;常用来处理实现功能类似数据类型不同的…

找PPT模板就上这5个网站~

分享几个可以永久免费下载PPT模板、素材的网站&#xff0c;上万个模板随便下载&#xff0c;赶紧收藏起来~ 1、菜鸟图库 https://www.sucai999.com/search/ppt/0_0_0_1.html?vNTYxMjky 网站素材非常全面&#xff0c;主要以设计类素材为主&#xff0c;办公类素材也很多&#x…

Spring MVC 接收 json 和返回 json (14)

目录 总入口 测试case 源码分析 1. 针对RequestBody的参数解析 2. 针对 ResponseBody 的返回值处理 总入口 通过上一篇Spring MVC 参数解析&#xff08;13&#xff09;_chen_yao_kerr的博客-CSDN博客的说明&#xff0c;相信大家对Sping MVC的参数解析有了一定的了解&…

8. 优先队列

8. 优先队列 普通的队列是一种先进先出的数据结构&#xff0c;元素在队列尾追加&#xff0c;而从队列头删除。在某些情况下&#xff0c;我们可能需要找出队列中的最大值或者最小值&#xff0c;例如使用一个队列保存计算机的任务&#xff0c;一般情况下计算机的任务都是有优先级…

【有功-无功协调优化】基于改进多目标粒子群优化算法(小生境粒子群算法)的配电网有功-无功协调优化研究(Matlab代码实现)

&#x1f4a5; &#x1f4a5; &#x1f49e; &#x1f49e; 欢迎来到本博客 ❤️ ❤️ &#x1f4a5; &#x1f4a5; &#x1f3c6; 博主优势&#xff1a; &#x1f31e; &#x1f31e; &#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 …

中断嵌套实验

使用汇编语言&#xff0c;要求&#xff1a; 外部中断1可以嵌套外部中断0 没有中断时&#xff0c;8个LED发光二极管以0.1s的速度闪烁。 有外部中断0时&#xff0c;8个LED发光二极管以0.1s的速度流水点亮。&#xff08;中断子程序0&#xff09; 有外部中断1时&#xff0c;会打断外…

gdb调试常用指令及案例讲解

文章目录 前言一、常用指令二、案例说明1、测试源文件2、编译和调试 三、其他指令四、案例说明 前言 GDB是一个由GNU开源组织发布的、UNIX/LINUX 操作系统下的、基于命令行的、功能强大的程序调试工具。 GDB 支持断点、单步执行、打印变量、观察变量、查看寄存器、查看堆栈等调…

每天一道大厂SQL题【Day22】华泰证券真题实战(四)

每天一道大厂SQL题【Day22】华泰证券真题实战(四) 大家好&#xff0c;我是Maynor。相信大家和我一样&#xff0c;都有一个大厂梦&#xff0c;作为一名资深大数据选手&#xff0c;深知SQL重要性&#xff0c;接下来我准备用100天时间&#xff0c;基于大数据岗面试中的经典SQL题&…

2023软件测试工程师必备技能?要卷,谁还不会了......

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 软件测试岗位是怎…

day15 消息队列

目录 消息队列 消息队列的使用 发送消息 消息的接收 消息队列的控制 消息队列 概念&#xff1a; 消息队列是system V IPC对象的一种&#xff1b; 消息队列有消息队列ID来唯一标识&#xff1b; 消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等&a…

zabbix故障排查

zabbix server服务问题可以查看server日志 tail -f /var/log/zabbix/zabbix_server.log 根据日志中的error报错提示分析原因 zabbix agent服务问题可以查看agent日志 tail -f /var/log/zabbix/zabbix_agentd.log 根据日志中的error报错提示分析原因 zabbix的nginx服务问题可…

从零开始学架构-计算高性能

一、概述 高性能是每个程序员的追求&#xff0c;无论做一个系统、还是写一组代码&#xff0c;都希望能够达到高性能的效果。而高性能又是最复杂的一环&#xff0c;磁盘、操作系统、CPU、内存、缓存、网络、编程语言、数据库、架构等&#xff0c;每个都可能影响系统的高性能&…

【FPGA实验1】FPGA点灯工程师养成记

对于FPGA几个与LED相关的实验&#xff08;包括按键点灯、流水灯、呼吸灯等&#xff09;的记录&#xff0c;方便日后查看。这世界上就又多了一个FPGA点灯工程师了&#x1f60f; 成为一个FPGA点灯工程师分三步&#xff1a; 一、按键点灯1、按键点灯程序2、硬件实现 二、流水灯1、…

智慧医院微信小程序定制开发功能有哪些

无论是哪个时代&#xff0c;人们对于医疗资源的需求都没有消退过&#xff0c;尤其是随着经济条件的提高&#xff0c;人们也越来越关注健康问题。无论是生病就诊还是定期体检都要用到医疗资源。但是平时到医院好像什么时候都人满为患&#xff0c;排很长时间的队&#xff0c;不仅…

ChatGPT4 的体验 一站式 AI工具箱 -—Poe(使用教程)

最近由于人工智能聊天机器人的爆火(ChatGPT)&#xff0c;因此各种各样的AI助手流行与网络&#xff0c;各种各样的都有&#xff0c;不论是什么样的其实都是为了我们更方便的解决问题&#xff0c;今天介绍一款AI工具箱——Poe将多种AI集成与一个界面&#xff0c;大家可以一次感受…

SQL Server基础 第五章 函数的使用(日期、字符串、时间、数学、转换等函数)

前言 在SQL Server中提供了许多内置函数&#xff0c;SQL Server中的内置函数就相当于Java、C#等编程语言中的内置API函数。按照函数种类可以分为聚合函数、数学函数、字符串函数、日期函数和时间函数、转换函数和元数据的数6种。在本章中重点讲解比较常用的4种函数&#xff0c…

在线题库整理及一些刷题注意事项

在线题库整理及一些刷题注意事项 刷题站CSDN编程语言支持 LeetCode编程语言支持数据库语言支持 牛客网编程语言支持数据库语言支持 洛谷编程语言支持 AcWing编程语言支持 蓝桥编程语言支持 做题的两种模式调用模式委托模式 注意事项小结 刷题站 老顾一个人单打独斗太久了&…