【并发编程】深入理解并发工具类CountDownLatch

news2025/2/4 6:54:55

文章目录

  • 前言
  • 一、初识 CountDownLatch
  • 二、CountDownLatch 的核心方法
  • 三、CountDownLatch 的应用场景
    • 应用场景一:等待多个线程任务执行完成
    • 应用场景二:等待外部资源初始化
    • 应用场景三:控制线程执行顺序
  • 四、CountDownLatch 的源码分析
    • 核心方法一:await()
    • 核心方法二:countDown()

前言

本篇文章想要讲解 JUC 工具类 CountDownLatch,因为 CountDownLatch 提供了简单有效的线程协调和控制机制,所以实际开发中是比较常用的,所以有必要了解一下 CountDownLatch

一、初识 CountDownLatch

CountDownLatch 作为 Java 中的一个同步工具类,用于在多线程间实现协调和控制,允许一个或多个线程等待其他线程完成操作后再继续执行。

CountDownLatch 内部维护了一个计数器,可以通过构造函数指定初始计数值。当一个线程完成了自己的任务后,可以调用 countDown() 方法将计数值减一。而其他线程可以通过调用 await() 方法等待计数值减为零,然后再继续执行。

一般情况下,主线程会创建 CountDownLatch 对象,然后传递给其他线程。其他线程执行完自己的任务后,调用 countDown() 方法进行计数,主线程调用 await() 方法等待计数值为零。

二、CountDownLatch 的核心方法

CountDownLatch 提供了四个核心方法来实现线程的协调和控制,核心方法如下:

  • public CountDownLatch(int count)

    • CountDownLatch 的构造方法,用于创建一个 CountDownLatch 对象,并指定初始计数值(计数值表示需要等待的线程数量)。
  • public void countDown()

    • 当一个线程完成任务后,可以调用该方法将计数器的值减一 (如果计数器的值已经为零,那么调用该方法没有任何影响,即计数器的值不会再减,而是一直为零)
  • public void await()

    • 当一个线程需要等待其他线程完成任务后再继续执行时,可以调用该方法进行等待 (如果计数器的值已经为零,那么调用该方法会立即返回)
    • 如果在等待过程中,当前线程被中断,则会抛出 InterruptedException 异常。
    • 需要注意的是调用该方法时,计数器的值应当在所有线程都能够完成任务后变为零,否则可能导致线程一直等待或提前继续执行的问题。
  • public boolean await(long timeout, TimeUnit unit)

    • await() 方法作用一样都能使当前线程等待,不同点在于允许设置超时时间 (即如果计数器的值在超时时间内变为零,那么方法会返回 true,否则返回 false)
    • 参数中的 timeout 表示超时时间的数值,unit 表示超时时间的单位。
    • 如果在等待过程中,当前线程被中断,则会抛出 InterruptedException 异常。

三、CountDownLatch 的应用场景

通过上面的介绍,应该能了解到 CountDownLatch 是什么以及如何使用,接下来通过具体的应用场景来看看 CountDownLatch 都可以在实际开发中起到怎样的作用。

应用场景一:等待多个线程任务执行完成

场景:如果需要等待多个线程执行完成后,才能进行下一步操作,就可以使用 CountDownLatch 来实现。通过创建一个 CountDownLatch 对象,并将计数器的值初始化为线程数(任务数),每个线程执行完成后,调用 countDown() 方法将计数器减一,主线程通过调用 await() 方法等待所有线程执行完成后执行下一步操作。

示例:有一个主线程需要等待五个子任务(线程)都完成后再进行后续操作(汇总子任务的结果)。

示例代码:

/**
 * CountDownLatch 示例
 * @author 单程车票
 */
public class CountDownLatchDemo {
    public static void main(String[] args) {
        // 任务数为5
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            int task = i;
            // 创建线程
            new Thread(() -> {
                try {
                    System.out.println("执行任务" + task + "业务");
                    try { TimeUnit.SECONDS.sleep(1);  } catch (InterruptedException e) {e.printStackTrace();}
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        // 阻塞直到所有任务执行完成或超出超时时间(30min)
        try {
            countDownLatch.await(30, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("子线程任务完成,主线程合并子线程结果");
    }
}

示例结果:

在这里插入图片描述

应用场景二:等待外部资源初始化

场景:当多个线程在执行前需要初始化某个系统组件或外部资源(如数据库连接池)时,可以使用 CountDownLatch 实现。通过主线程创建 CountDownLatch 对象,设定计数值为 1。初始化线程在完成资源初始化后调用 countDown() 方法,然后其他线程通过 await() 方法等待初始化完成后再开始使用资源。

示例:有三个线程等待外部资源初始化线程执行完成后再执行各自线程的业务。

示例代码:

/**
 * CountDownLatch 示例
 * @author 单程车票
 */
public class CountDownLatchDemo {
    public static void main(String[] args) {
        // 初始计数值为1
        CountDownLatch countDownLatch = new CountDownLatch(1);
        // 三个线程等待外部资源线程初始化后在执行
        for (int i = 0; i < 3; i++) {
            int task = i;
            // 创建线程
            new Thread(() -> {
                // 阻塞直到外部资源初始化完成
                try {
                    countDownLatch.await(30, TimeUnit.MINUTES);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("外部资源初始化完成,执行任务" + task + "业务");
            }).start();
        }
        // 创建线程进行外部资源初始化
        new Thread(() -> {
            try {
                System.out.println("初始化外部资源");
                try { TimeUnit.SECONDS.sleep(1);  } catch (InterruptedException e) {e.printStackTrace();}
            } finally {
                countDownLatch.countDown();
            }
        }).start();
    }
}

示例结果:
在这里插入图片描述

应用场景三:控制线程执行顺序

场景:当需要保证多个线程按照特定的顺序执行时,可以通过 CountDownLatch 实现。主线程可以根据特定执行顺序创建多个 CountDownLatch 对象对应多个线程,每个 CountDownLatch 对象的初始计数值都为 1,保证某一时刻只有指定顺序的线程执行,执行完成后,调用下一个 CountDownLatch 对象的 countDown() 方法唤醒下一个指定顺序线程执行。

示例:有三个线程,需要按照 3 1 2 的顺序依次执行各自线程的业务。

示例代码:

/**
 * CountDownLatch 示例
 * @author 单程车票
 */
public class CountDownLatchDemo {
    public static void main(String[] args) {
        // 初始计数值为1
        CountDownLatch order1 = new CountDownLatch(1);
        CountDownLatch order2 = new CountDownLatch(1);
        CountDownLatch order3 = new CountDownLatch(1);
        // 三个线程按照 3 1 2 的顺序执行
        order3.countDown();  // 开启多个线程顺序执行
        // 创建线程1
        new Thread(() -> {
            // 阻塞直到线程3完成
            try {
                order1.await(30, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println("执行任务 1 的业务");
            } finally {
                order2.countDown();
            }
        }).start();
        // 创建线程2
        new Thread(() -> {
            // 阻塞直到线程1完成
            try {
                order2.await(30, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行任务 2 的业务");
        }).start();
        // 创建线程3
        new Thread(() -> {
            // 阻塞直到主线程开启顺序执行
            try {
                order3.await(30, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println("执行任务 3 的业务");
            } finally {
                order1.countDown();
            }
        }).start();
    }
}

示例结果:

在这里插入图片描述

四、CountDownLatch 的源码分析

通过前两部分的内容可以了解到 CountDownLatch 的使用方式和应用场景了,可以看到CountDownLatch 最为核心的两个方法是 countDown()await()。接下来通过源码分析来看看这两个方法是如何实现的。

通过源码可以看到 CountDownLatch 其实是基于 AQS 实现的, CountDownLatch 内部通过一个静态内部类 Sync 继承 AQS 来实现构建同步锁。下面从 countDown()await() 这两个方法开始进行源码分析。

核心方法一:await()

await() 源码:
在这里插入图片描述

可以看到 await() 方法中调用了 Sync 的 acquireSharedInterruptibly() 方法,但是 Sync 中并没有实现该方法,所以实际上调用的是 AQS 中的 acquireSharedInterruptibly() 方法,进入方法:
在这里插入图片描述

方法中先判断线程是否被中断,如果被中断则抛出 InterruptedException 异常,通过调用 tryAcquireShared() 方法尝试抢占共享锁,这个方法是 AQS 的抽象方法由子类实现,这里实际上调用的就是 Sync 的 tryAcquireShared() 方法,进入方法:
在这里插入图片描述

该方法调用 getState() 方法获取当前计数器的值,并判断是否为 0,若为 0 则返回 1,不为 0 则返回 -1。回到上面的 tryAcquireShared() 中可以看到当计数器的值为 0 时则不需要进入等待队列,当计数器的值不为 0 时,则调用 doAcquireSharedInterruptibly())。进入方法:
在这里插入图片描述

深入方法代码可以分为以下几步:

  • 首先通过 addWaiter() 构建一个共享模式的 Node 并加入等待队列。

  • 然后通过无限循环,判断当前节点的前驱节点是否是头节点(前驱节点为头节点表示意味着具有尝试资源获取的机会)

    • 前驱节点是头节点,则不断地尝试获取资源(即调用 tryAcquireShared() 这个方法前面有提到,用于判断计数器的值是否为 0),计数值为 0,则表示获取资源成功,即线程可以运行,所以执行 setHeadAndPropagate() 将当前节点设置为新的头结点,并设置 p.next=null 等待 GC 回收。
    • 前驱结点不是头节点,则执行 shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() 根据一定条件判断线程是否应该被阻塞并检查是否发生中断,等待后续唤醒。
  • 最后的 finally 通过标志 failed (表示是否获取资源失败),如果为 true,则执行 cancelAcquire() 方法取消对资源的获取,并移出等待队列。

所以这个方法核心为通过无限循环不断地尝试获取共享资源,获取成功则将当前节点设置为头结点,获取失败则判断是否需要阻塞并检查是否被中断,如果最后获取失败,则放弃获取资源并移出等待队列。

到这里就是 await() 方法的整个实现流程了,底层通过调用 AQS 的 doAcquireSharedInterruptibly() 方法以及 CountDownLatch 实现 AQS 的抽象方法 tryAcquireShared() 实现线程阻塞和唤醒。

核心方法二:countDown()

countDown() 源码:
在这里插入图片描述

可以看到 countDown() 方法中调用了 Sync 的 releaseShared() 方法,但是 Sync 中并没有实现该方法,所以实际上调用的是 AQS 中的 releaseShared() 方法,进入方法:
在这里插入图片描述

方法中调用 Sync 实现 AQS 的抽象方法 tryReleaseShared() 来进行判断,进入方法:
在这里插入图片描述

方法中判断当前计数器值是否为 0,是则返回 false 不做任何操作,也就是当计数器值为 0 时调用 CountDownLatch() 方法不会做任何操作。不是 0 则进行计数器值减一,并通过 CAS 操作更新计数器值,如果更新后的值为 0,则调用 AQS 内部的 doReleaseShared() 方法释放共享资源,否则除了更新计数器值之外不做任何操作。进入 doReleaseShared() 方法:
在这里插入图片描述

doReleaseShared() 方法的目的是在释放共享资源时,确保唤醒等待的线程,并通过循环和 CAS 操作来处理并发情况和头节点的变化。

到这里就是 countDown() 方法的整个实现过程了,底层通过 CountDownLatch 实现 AQS 的抽象方法 tryReleaseShared() 采用 CAS 来完成计数器减一,并通过 AQS 的内部方法 doReleaseShared() 实现释放资源。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql表主键自增过大问题

问题及项目环境 问题 最近在做项目时&#xff0c;发现我创建的每一个表的主键设置自增&#xff0c;在插入数据数据时会出现自增值过大的问题。 问题展示: 在后端执行Basemapper中的insert()方法时&#xff0c;数据库中的主键id字段为下: 且我在对应的实体类的设置为下: 我们…

mysql四种隔离级别以及原理

事务4大特性&#xff1a; 原子性&#xff1a; undolog日志来保证&#xff0c; 记录了要回滚的日志信息&#xff0c; 事务回滚时撤销已经执行的sql. 一致性&#xff1a;由其它3大特性来保证。 隔离性: MVCC来保证&#xff0c; 多版本并发控制。 持久性&#xff1a;由redolog来保…

flask路由、模板、请求与响应、闪现以及请求扩展

一、函数加装饰器的执行顺序 flask的路由基于装饰器---->在视图函数上再加装饰器---->加多个装饰器的执行顺序---->登录认证装饰器---->加载router下,先做路由匹配,匹配成功执行被auth包裹的视图函数 二、路由系统 flask的路由是基于装饰器的---->但是它的…

APB/AHB/AXI总线介绍和理解

APB/AHB/AXI总线介绍&#xff1a; APB/AHB/AXI均属于AMBA (Advanced Microcontroller Bus Architecture)&#xff0c;常用于SoC设计中&#xff0c;全称叫作高级微控制器总线架构&#xff0c;它是由ARM公司研发推出的高性能片上总线协议。 AMBA 1.0还包含ASB和APB&#xff08;A…

MP4格式视频怎么转mov格式?好用的视频格式转换方法分享

MOV格式是苹果公司的专有格式&#xff0c;因此在苹果设备上播放MOV格式的视频时&#xff0c;兼容性更好&#xff0c;因此可以实现更高质量的视频。如果我们需要高质量的视频输出&#xff0c;将MP4转换为MOV格式可能是个好选择。那么怎么进行转换呢&#xff1f;给大家分享几种简…

Linux环境下,Nginx+Naocs远程访问碰到的若干问题

一、配置背景 该项目来源于尚硅谷SpringCloud进阶课程&#xff0c;在linux环境下配置一个注册中心组。 二、碰到的问题 问题主要是远程访问Nginx显示无法连接的问题&#xff0c;接下来是排查方案&#xff1a; 1. 防火墙问题 这里需要确保双方电脑能ping通之后确保端口也能…

k210获取机器码

准备工作&#xff1a; kflash_gui&#xff08;下载固件到开发板&#xff09; key_gen_v1.2&#xff08;需要将其下载至开发板&#xff09; 1.kflash_gui 下载地址&#xff1a;Releases sipeed/kflash_gui GitHub 2. key_gen_v1.2下载地址&#xff1a;下载站 - Sipeed 3.…

Java经典面试解析:服务器卡顿、CPU飙升、接口负载剧增

01 线上服务器CPU飙升&#xff0c;如何定位到Java代码 解决这个问题的关键是要找到Java代码的位置。下面分享一下排查思路&#xff0c;以CentOS为例&#xff0c;总结为4步。 第1步&#xff0c;使用top命令找到占用CPU高的进程。 第2步&#xff0c;使用ps –mp命令找到进程下…

Three.js——十三、自定义大小画布、UI交互按钮以及3D场景交互、渲染画布为文件(图片)

画布全屏以及自定义大小画布 <!-- canvas元素默认是行内块元素 --> <divclass"model"style"background-color: #ff0000;"width"300"height"180" ></div>画布随窗口变化 // 画布跟随窗口变化 window.onresize fun…

cmake的一个测试demo

目录 一、ubuntu中安装cmake二、单个源文件main.cCMakeLists.txt的编写 三、多个源文件main.ctest1.ctest1.hCMakeLists.txt的编写 一、ubuntu中安装cmake sudo apt-get install cmake查看cmake的版本号 cmake --version二、单个源文件 main.c #include<stdio.h>int …

【微信小程序-uniapp】CustomButton 自定义常用吸底按钮组件

1. 效果图 2. 组件完整代码 <template><view:class="[custom-btn flex-center, size == big ? big : mid, type == primary ? primary : info, plain ? plain : , disabled ? disabled : , round ? round : ]"

苹果笔买原装的还是随便买?ipad触控笔推荐

当像iPad这样的平板电脑功能变得越来越强&#xff0c;能够承载的功能也会越来越多&#xff0c;并且会越来越多地渗透到我们的日常生活和工作中。随着电子设备的更新和软件的不断完善&#xff0c;电容笔的性能也在不断的提升&#xff0c;因此如何挑选一款相对好用的电容笔&#…

【Kafka】Ubuntu 部署kafka中间件,实现Django生产和消费

原文作者&#xff1a;我辈李想 版权声明&#xff1a;文章原创&#xff0c;转载时请务必加上原文超链接、作者信息和本声明。 文章目录 前言一、Kafka安装1.下载并安装Java2.下载和解压 Kafka3.配置 Kafka4.启动 Kafka5.创建主题和生产者/消费者6.发布和订阅消息 二、KafkaDjang…

AIS报文解析

!AIVDM,1,1,A,13u?etPv2;0n:dDPwUM1U1Cb069D,023* 我们知道消息内容就是13u?etPv2;0n:dDPwUM1U1Cb069D&#xff0c;这是一串ASCII码字符串&#xff0c;我们可以获取其对应的ASCII码数值。 但是在AIS的编码表不需要这么多符号&#xff0c;所以截取了其中一部分&#xff0c;如…

良心推荐!5款支持Linux系统的国产软件,兼容国产操作系统

虽然市面上大多数用户使用的是Windows操作系统&#xff0c;但也有不少使用Linux系统的用户&#xff0c;特别是国产操作系统的崛起&#xff0c;让Linux系统阵营的用户越来越多。Linux不像Windows那样&#xff0c;有着完整的生态环境丰富的软件应用&#xff0c;但也逐渐在完善中&…

探秘Session跨页面传递数据的神奇力量

探秘Session跨页面传递数据的神奇力量 前言一、什么是 Session 会话?二、如何创建 Session 和获取(id 号,是否为新)三、Session 域数据的存取四、Session 生命周期控制五、Session的销毁五、浏览器和 Session 之间关联的技术内幕 前言 本博主将用CSDN记录软件开发求学之路上亲…

自定义类型详解(结构体、枚举、联合)

目录 一、结构体 1.1结构体的认识&#xff1a; 1.2结构体的声明 1.先声明结构体类型&#xff0c;再定义该类型的变量 2.在声明类型的同时定义 1.3结构体的特殊声明 1.4结构体的自引用 1.5结构体的初始化和访问 1.6结构体内存对齐 1.7修改默认对齐数 1.8结构体传参 二…

驱动程序和应用程序

驱动程序和应用程序 一、应用程序和驱动程序如何关联起来的 1、文件描述符fp 与 struct file 应用程序&#xff08;APP&#xff09;在打开文件时&#xff0c;可以得到一个整数&#xff0c;这个整数被称为文件句柄。对于 APP 的每一个文件句柄&#xff0c;在内核里面都有一个…

AI辅助瞄准系统开发与实战(三)-竣工

文章目录 前言GUI功能整合提示框功能整合 总体代码自定义线程池YoloDectect工具类窗口绘制鼠标控制控制器GUI界面 总结 前言 okey&#xff0c;大概经过&#xff0c;两天的开发&#xff0c;我在这里完成了基本的全部开发。 那么我们先来看看大概的效果吧&#xff1a; 在这里的…

Vue3通透教程【十八】TS为组件的props标注类型

文章目录 &#x1f31f; 写在前面&#x1f31f; 回顾defineProps的基础写法&#x1f31f; defineProps的TS写法&#x1f31f; withDefaults方法&#x1f31f; 拓展&#x1f31f; 写在最后 &#x1f31f; 写在前面 专栏介绍&#xff1a; 凉哥作为 Vue 的忠实 粉丝输出过大量的 …