在 Java 并发编程领域,JDK 提供的工具类是简化多线程协作的重要武器。这些工具类基于 AQS(AbstractQueuedSynchronizer)框架实现,封装了复杂的同步逻辑,让开发者无需深入底层即可实现高效的线程协作。本文作为并发工具类系列的第一篇,将重点解析CountDownLatch和Semaphore的核心原理、典型使用场景及实战案例,帮助开发者掌握其在多线程协作中的应用技巧。

一、CountDownLatch:等待多线程完成的计数器

CountDownLatch(倒计时门闩)是一种经典的线程同步工具,其核心功能是让一个或多个线程等待其他线程完成指定操作后再继续执行。它通过一个递减的计数器实现线程间的协调,计数器归零时,所有等待的线程将被唤醒。

1.1 核心原理与方法解析

CountDownLatch的设计基于 “计数器 + 等待唤醒” 机制,核心方法如下:

方法

功能描述

CountDownLatch(int count)

构造方法,初始化计数器值(count为需要等待的线程操作数,必须≥0)

void await()

调用线程进入阻塞状态,直至计数器归 0 或被中断

boolean await(long timeout, TimeUnit unit)

带超时时间的等待,超时后无论计数器是否归 0,线程都会唤醒并返回false

void countDown()

将计数器值减 1,当值为 0 时,唤醒所有因await()阻塞的线程

关键特性:CountDownLatch的计数器是一次性的,一旦归 0,后续调用countDown()不会再改变其状态,因此无法重复使用。

1.2 典型场景:主线程等待子线程初始化完成

在大型应用启动过程中,主线程往往需要等待多个初始化任务(如加载配置文件、初始化数据库连接、预热缓存等)完成后才能启动核心业务。CountDownLatch完美适配这种 “等待多任务完成” 的场景。

实战案例

public class SystemInitDemo {// 初始化计数器,需等待3个核心任务完成private static final CountDownLatch initLatch = new CountDownLatch(3);public static void main(String[] args) throws InterruptedException {System.out.println("系统启动:开始等待初始化任务...");// 启动配置加载任务new Thread(() -> {try {System.out.println("配置加载任务:开始加载系统配置...");Thread.sleep(1500); // 模拟配置加载耗时System.out.println("配置加载任务:完成加载");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {initLatch.countDown(); // 任务完成,计数器减1}}, "配置线程").start();// 启动数据库连接任务new Thread(() -> {try {System.out.println("数据库任务:开始建立连接池...");Thread.sleep(2000); // 模拟连接池初始化耗时System.out.println("数据库任务:连接池初始化完成");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {initLatch.countDown();}}, "数据库线程").start();// 启动缓存预热任务new Thread(() -> {try {System.out.println("缓存任务:开始预热热点数据...");Thread.sleep(1000); // 模拟缓存预热耗时System.out.println("缓存任务:热点数据预热完成");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {initLatch.countDown();}}, "缓存线程").start();// 主线程等待所有初始化任务完成initLatch.await();System.out.println("系统启动:所有初始化任务完成,启动核心服务...");}
}

运行结果

系统启动:开始等待初始化任务...配置加载任务:开始加载系统配置...数据库任务:开始建立连接池...缓存任务:开始预热热点数据...缓存任务:热点数据预热完成配置加载任务:完成加载数据库任务:连接池初始化完成系统启动:所有初始化任务完成,启动核心服务...

案例解析

  • 主线程通过initLatch.await()阻塞等待,直到 3 个初始化线程都调用countDown()使计数器归 0;
  • 即使各任务执行时间不同(数据库任务耗时最长),主线程也会等待所有任务完成后再继续,确保系统启动的完整性;
  • finally块中调用countDown()保证即使任务异常,计数器也能正确递减,避免主线程无限等待。

1.3 反向应用:子线程等待主线程指令

CountDownLatch不仅能让主线程等待子线程,还能通过反向设计实现 “子线程等待主线程信号”。例如在并发测试中,让所有测试线程准备就绪后,等待主线程发出 “开始” 指令,确保所有线程同时执行测试代码,消除启动顺序带来的误差。

示例代码

public class ConcurrentTestDemo {// 计数器初始化为1,代表主线程的"开始"信号private static final CountDownLatch startSignal = new CountDownLatch(1);// 记录并发执行结果private static final AtomicInteger result = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {int threadCount = 5; // 并发线程数// 启动5个测试线程for (int i = 0; i < threadCount; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + ":准备就绪,等待开始信号");startSignal.await(); // 等待主线程指令// 收到信号后执行并发操作result.incrementAndGet();System.out.println(Thread.currentThread().getName() + ":执行完成");} catch (InterruptedException e) {Thread.currentThread().interrupt();}}, "测试线程-" + i).start();}// 主线程准备3秒后发出开始信号Thread.sleep(3000);System.out.println("主线程:发出开始信号");startSignal.countDown(); // 计数器归0,唤醒所有测试线程// 等待所有测试线程完成(实际场景可再用一个CountDownLatch)Thread.sleep(1000);System.out.println("所有线程执行完成,最终结果:" + result.get()); // 预期结果为5}
}

核心价值:通过startSignal确保所有子线程在同一时间点开始执行,真实模拟高并发场景,提升测试准确性。

二、Semaphore:控制资源并发访问的信号量

Semaphore(信号量)是用于控制资源并发访问数量的工具类,它通过维护一组 “许可”(permit)实现对资源的限流。线程需要先获取许可才能访问资源,访问结束后释放许可,供其他线程使用。

2.1 核心原理与方法解析

Semaphore的核心是 “许可管理”,通过控制许可数量限制并发线程数,核心方法如下:

方法

功能描述

Semaphore(int permits)

构造方法,初始化许可数量(permits为允许同时访问的线程数)

Semaphore(int permits, boolean fair)

带公平性参数的构造方法,fair=true时按线程请求顺序分配许可

void acquire()

获取 1 个许可,若暂时无可用许可,线程会阻塞等待

boolean tryAcquire()

尝试获取 1 个许可,立即返回结果(成功true/ 失败false),不阻塞

boolean tryAcquire(long timeout, TimeUnit unit)

超时尝试获取许可,超时未获取则返回false

void release()

释放 1 个许可,将其归还给信号量

int availablePermits()

返回当前可用的许可数量

关键特性:Semaphore的许可数量可以动态调整,release()方法可在未获取许可的情况下释放,从而增加总许可数(需谨慎使用)。

2.2 典型场景:资源池的并发访问控制

数据库连接池、线程池等资源池场景中,资源数量有限,Semaphore可用于限制同时访问资源的线程数,防止因资源耗尽导致的系统异常。

实战案例

public class ConnectionPoolDemo {// 数据库连接池(模拟10个连接)private static final int POOL_SIZE = 10;private static final List<Connection> connectionPool = new ArrayList<>(POOL_SIZE);// 信号量控制并发访问,许可数等于连接池大小private static final Semaphore semaphore = new Semaphore(POOL_SIZE, true); // 公平模式// 初始化连接池static {for (int i = 0; i < POOL_SIZE; i++) {connectionPool.add(new MockConnection("连接-" + (i + 1)));}}// 获取数据库连接public static Connection getConnection() throws InterruptedException {semaphore.acquire(); // 获取许可(若连接池满则等待)synchronized (connectionPool) {return connectionPool.remove(0); // 从池内取出连接}}// 释放数据库连接public static void releaseConnection(Connection connection) {if (connection != null) {synchronized (connectionPool) {connectionPool.add(connection); // 连接放回池内}semaphore.release(); // 释放许可}}// 模拟数据库连接类static class MockConnection {private String name;MockConnection(String name) { this.name = name; }@Overridepublic String toString() { return name; }}public static void main(String[] args) {// 模拟20个线程并发请求连接for (int i = 0; i < 20; i++) {new Thread(() -> {Connection conn = null;try {conn = getConnection();System.out.println(Thread.currentThread().getName() + "获取到" + conn + ",当前可用许可:" + semaphore.availablePermits());Thread.sleep(1000); // 模拟数据库操作耗时} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {releaseConnection(conn);if (conn != null) {System.out.println(Thread.currentThread().getName() + "释放了" + conn + ",当前可用许可:" + semaphore.availablePermits());}}}, "业务线程-" + i).start();}}
}

运行结果片段

业务线程-0获取到连接-1,当前可用许可:9业务线程-1获取到连接-2,当前可用许可:8...业务线程-9获取到连接-10,当前可用许可:0// 此时许可耗尽,线程10-19进入等待业务线程-0释放了连接-1,当前可用许可:1业务线程-10获取到连接-1,当前可用许可:0

...

案例解析

  • Semaphore通过 10 个许可限制同时使用连接的线程数,与连接池容量匹配,避免资源过度占用;
  • 公平模式(fair=true)确保线程按请求顺序获取许可,减少饥饿现象;
  • getConnection()和releaseConnection()通过同步块保证连接池操作的线程安全,结合信号量实现完整的资源管控。

2.3 扩展场景:接口限流与流量控制

Semaphore可用于接口限流,通过控制单位时间内的请求数保护系统稳定。例如限制某 API 每秒最多处理 100 个请求,超出部分直接拒绝或排队等待。

示例代码

public class ApiRateLimiter {private final Semaphore semaphore;private final int maxRequestsPerSecond; // 每秒最大请求数public ApiRateLimiter(int maxRequestsPerSecond) {this.maxRequestsPerSecond = maxRequestsPerSecond;this.semaphore = new Semaphore(maxRequestsPerSecond);// 定时任务:每秒重置许可数量ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);scheduler.scheduleAtFixedRate(() -> {int permitsToRelease = maxRequestsPerSecond - semaphore.availablePermits();if (permitsToRelease > 0) {semaphore.release(permitsToRelease); // 补充许可至上限}}, 1, 1, TimeUnit.SECONDS);}// 尝试访问APIpublic boolean tryAccess() {return semaphore.tryAcquire();}public static void main(String[] args) {// 限制每秒最多5个请求ApiRateLimiter limiter = new ApiRateLimiter(5);// 模拟10个并发请求for (int i = 0; i < 10; i++) {new Thread(() -> {if (limiter.tryAccess()) {System.out.println(Thread.currentThread().getName() + ":API访问成功");} else {System.out.println(Thread.currentThread().getName() + ":API访问被限流");}}, "请求线程-" + i).start();}}
}

运行结果

请求线程-0:API访问成功

请求线程-1:API访问成功

请求线程-2:API访问成功

请求线程-3:API访问成功

请求线程-4:API访问成功

请求线程-5:API访问被限流

请求线程-6:API访问被限流

...

限流原理:通过定时任务每秒补充许可,使Semaphore的许可数始终维持在maxRequestsPerSecond,从而实现固定速率的流量控制。

三、CountDownLatch 与 Semaphore 的对比与协同

特性

CountDownLatch

Semaphore

核心功能

等待多个线程完成操作

控制并发访问资源的线程数

计数器特性

一次性递减,归 0 后不可重置

可重复获取和释放,动态调整

典型场景

初始化协调、并发测试同步

资源池控制、接口限流

线程协作方向

多线程→主线程(或反之)

线程间竞争资源

协同案例:在分布式任务调度中,可用CountDownLatch等待所有任务节点准备就绪,再用Semaphore控制同时执行任务的节点数,实现 “先同步准备,再限流执行” 的流程。

总结

CountDownLatch和Semaphore是解决多线程协作问题的利器:CountDownLatch通过计数器实现线程间的等待协调,适合初始化、测试同步等场景;Semaphore通过许可管理控制资源并发访问,适合资源池、限流等场景。掌握这两个工具类的核心原理和使用技巧,能显著提升并发编程的效率和可靠性。

下一篇将介绍CyclicBarrier、Phaser等其他常用工具类,敬请期待。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Go 工程化全景:从目录结构到生命周期的完整服务框架

今天天气很好, 正好手头有个小项目, 整理了一下中小项目标准化的痛点问题, 如下, 希望可以帮到大家. 一个成熟的 Go 项目不仅需要清晰的代码组织&#xff0c;还需要完善的生命周期管理。本文将详细讲解生产级 Go 服务的目录设计&#xff08;包含 model 等核心目录&#xff09;、…

【C++】2. 类和对象(上)

文章目录一、类的定义1、类定义格式2、访问限定符3、类域二、实例化1、实例化概念2、对象⼤⼩三、this指针四、C和C语⾔实现Stack对⽐一、类的定义 1、类定义格式 class为定义类的关键字&#xff0c;Stack为类的名字&#xff0c;{ }中为类的主体&#xff0c;注意类定义结束时…

UnityURP 扭曲屏幕效果实现

UnityURP 扭曲屏幕效果实现前言项目下载URPGrabPass空间扭曲着色器实现添加可视化控制创建材质球并设置补充粒子使用步骤CustomData映射移动设备优化鸣谢前言 在Unity的Universal Render Pipeline (URP) 中&#xff0c;传统的GrabPass功能被移除&#xff0c;借助URPGrabPass工…

(三)软件架构设计

2024年博主考软考高级系统架构师没通过&#xff0c;于是决定集中精力认真学习系统架构的每一个环节&#xff0c;并在2025年软考中取得了不错的成绩&#xff0c;虽然做信息安全的考架构师很难&#xff0c;但找对方法&#xff0c;问题就不大&#xff01; 本文主要是博主在学习过程…

切记使用mt19937构造随机数

在做 Kazaee CodeForces - 1746F 这个问题的时候&#xff0c;最初的时候使用了ran()&#xff0c;然后一直WA&#xff0c;遂改成mt19937&#xff0c;顺利通过本道题。 mt19937 Rand(time(0)); 调用随机数时候&#xff0c;使用&#xff1a; Rand() & 1 注意看&#xff0…

基于N32G45x+RTT驱动框架的定时器外部计数

时钟选择 高级控制定时器的内部时钟:CK_INT: 两种外部时钟模式: 外部输入引脚 外部触发输入 ETR 内部触发输入(ITRx):一个定时器用作另一个定时器的预分频器 外部时钟原理 通过配置 TIMx_SMCTRL.SMSEL=111 选择该模式。 计数器可以配置为在所选输入的时钟上升沿或下降沿 …

[特殊字符] Ubuntu 下 MySQL 离线部署教学(含手动步骤与一键脚本)

适用于 Ubuntu 20.04 / 22.04 无网络环境部署 MySQL。 建议初学者先按手动方式部署一遍理解原理&#xff0c;再使用自动化脚本完成批量部署。&#x1f4c1; 一、准备工作 ✅ 1. 虚拟机环境 系统&#xff1a;Ubuntu 22.04&#xff08;或兼容版本&#xff09;环境&#xff1a;无网…

系统一个小时多次Full GC,导致系统线程停止运行,影响系统的性能,可靠性

背景&#xff1a; 某一天系统出现了请求超时&#xff0c;然后通过日志查看&#xff0c;程序执行到某一个位置&#xff0c;直接停下来来了&#xff0c;或者说所有的线程的执行都停下来了。而且是该时间段&#xff0c;请求处理变慢。排查相关的服务&#xff0c;并没有出现死锁&am…

使用OMV+NextCloud搭建私有云

原文地址&#xff1a;使用OMVNextCloud搭建私有云 – 无敌牛 欢迎参观我的网站&#xff1a;无敌牛 – 技术/著作/典籍/分享等 OpenMediaVault&#xff08;简称OMV&#xff09;是一款基于Debian的开源网络存储&#xff08;NAS&#xff09;操作系统&#xff0c;提供Web管理界面&…

Codeforces Round 1008 (Div. 2)

A. Final Verdict 题目大意 给你一个数组a&#xff0c;每次把他拆分为等长的k个子序列&#xff0c;然后用子序列的平均数替换掉这个子序列&#xff0c;问最后能不能让数组只剩下一个数字x 解题思路 无论怎么划分&#xff0c;最后的总值是不变的&#xff0c;所以只需要看总和…

python转移安装目录到D盘

迁移python安装路径第一步&#xff1a;移动目录第二步&#xff1a;修改环境变量之前没有设置之前设置过第一步&#xff1a;移动目录 源路径&#xff1a; C:\Users\Emma.ZRF\AppData\Local\Programs\Python\Python38 原环境变量 C:\Users\Emma.ZRF\AppData\Local\Programs\Pyth…

C#垃圾回收机制:原理与实践

C#垃圾回收机制:原理与实践 一、垃圾回收:C#内存管理的“幕后功臣”​ 二、GC的核心引擎:基于代的优化策略 三、Demo展示 1. 简单对象的垃圾回收示例 2. 基于代的回收示例 四、常用方法 五、推荐使用的场景 六、注意事项 管住手:避免滥用 GC.Collect() 析构函数:保持轻量 …

基于SpringBoot+MyBatis+MySQL+VUE实现的名城小区物业管理系统(附源码+数据库+毕业论文+开题报告+部署教程+配套软件)

摘要 当下&#xff0c;正处于信息化的时代&#xff0c;许多行业顺应时代的变化&#xff0c;结合使用计算机技术向数字化、信息化建设迈进。以前相关行业对于物业信息的管理和控制&#xff0c;采用人工登记的方式保存相关数据&#xff0c;这种以人力为主的管理模式已然落后。本人…

3DXML 转换为 UG 的技术指南及迪威模型网在线转换推荐

一、3DXML 转换为 UG 的必要性 &#xff08;一&#xff09;软件功能利用需求 3DXML 格式由达索系统开发&#xff0c;主要用于在其相关产品&#xff08;如 CATIA、SOLIDWORKS 和 3DEXPERIENCE 等&#xff09;中进行 3D 数据交换与轻量化可视化。它虽然能够很好地在达索生态内实…

无人机光伏巡检缺陷检出率↑32%:陌讯多模态融合算法实战解析

原创声明本文为原创技术解析&#xff0c;引用来源标注 “陌讯技术白皮书”&#xff0c;禁止未经授权的转载与改编。摘要在无人机光伏巡检场景中&#xff0c;边缘计算优化与复杂场景鲁棒性是提升检测效率的核心挑战。本文解析陌讯多模态融合算法在光伏板热斑、隐裂等缺陷检测中的…

仓库管理系统-15-前端之管理员管理和用户管理

文章目录 1 后台查询用户列表 1.1 null和空字符串的检查 1.2 UserController.java 2 管理员管理 2.1 传递参数roleId=1 2.2 admin/AdminManage.vue 3 用户管理 3.1 传递参数roleId=2 3.2 user/UserManage.vue 管理员管理和用户管理,与之前的Main.vue的内容基本一致,无非是管理…

个人笔记UDP

UDP消息发送发送端​ import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; ​ //不需要连接服务器 public class UdpClientDemo01 {public static void main(String[] args) throws Exception {/…

26届算法秋招_baidu笔试_算法编程题。

给定2个字符串str1、str2&#xff0c;计算把str1转变为str2的最小操作数。可执行的操作有&#xff1a;插入一个字符修改一个字符删除一个字符解题&#xff1a;这是一个经典的编辑距离问题&#xff0c;通常使用动态规划解决。定义dp[i][j]表示将str1的前i个字符转换为str2的前j个…

uniapp-vue3来实现一个金额千分位展示效果

前言&#xff1a;uniapp-vue3来实现一个金额千分位展示效果实现效果&#xff1a;实现目标&#xff1a;1、封装组件&#xff0c;组件内部要实现&#xff0c;input输入金额后&#xff0c;聚焦离开后&#xff0c;金额以千分位效果展示&#xff0c;聚焦后展示大写金额的弹框随时写的…

途游Android面试题及参考答案

对 Java 面向对象的理解是什么?多态的实现方法有哪些? Java 面向对象是一种编程思想,核心在于将现实世界中的事物抽象为 “对象”,每个对象由 “属性”(数据)和 “方法”(行为)组成,通过对象之间的交互完成功能。其核心特性包括封装、继承和多态: 封装是指将对象的属…