并发编程——08 Semaphore源码分析

1 概述

  • Semaphore 是基于 AQS + CAS 实现的,可根据构造参数的布尔值,选择使用公平锁,还是非公平锁。Semaphore 默认使用非公平锁;

    在这里插入图片描述

2 构造函数

// AQS的实现
private final Sync sync;// 默认使用非公平锁
public Semaphore(int permits) {sync = new NonfairSync(permits);
}// 根据fair布尔值选择使用公平锁还是非公平锁
public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
  • syncSemaphore 内部用于实现同步控制的核心组件,它基于 **AQS(AbstractQueuedSynchronizer,抽象队列同步器)**实现。AQS 是 Java 并发包中同步组件(如锁、信号量)的“基石”,通过维护同步状态和等待队列来实现线程的同步与协作;

  • 构造函数 public Semaphore(int permits)

    • 入参 permits 表示信号量的许可数量(即同时允许多少个线程访问共享资源);
    • 该构造函数默认创建 NonfairSync 实例(非公平锁实现);
    • 非公平锁的特点:线程获取许可时不会严格遵循“先到先得”,新线程可能直接抢占许可,导致等待队列中的线程长时间阻塞,但吞吐量通常更高;
  • 构造函数 public Semaphore(int permits, boolean fair)

    • 入参 fair 是布尔值,用于指定是否使用公平锁
    • fairtrue,则创建 FairSync 实例(公平锁实现);若为 false,则创建 NonfairSync 实例(非公平锁);
    • 公平锁的特点:线程会严格按照“等待时间先后”获取许可,保证了等待队列中线程的公平性,但由于需要维护队列顺序,吞吐量可能略低。

3 公平锁与非公平锁

  • Semaphore 中公平锁与非公平锁的实现,可以在tryAcquireShared()方法中找到两种锁的区别;

    在这里插入图片描述

3.1 NonfairSync

  • Semaphore#NonfairSync#tryAcquireShared(int acquires)

    // 非公平锁,获取信号量
    protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);
    }
    
    • 该方法是 NonfairSync(非公平锁实现类)中对 AQS 共享式获取逻辑的实现;
    • 它直接调用 nonfairTryAcquireShared(acquires) 方法,把“非公平获取信号量”的核心逻辑委托给该方法处理;
  • Semaphore#Sync#nonfairTryAcquireShared(int acquires)

    // 非公平锁,获取信号量
    final int nonfairTryAcquireShared(int acquires) {// 自旋for (;;) {// 获取Semaphore中可用的信号量数int available = getState();// 当前可用信号量数 - acquiresint remaining = available - acquires;// 可用信号量数不足 或 CAS操作获取信号量失败,返回  当前可用信号量数 - acquiresif (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
    }
    
    • 这个方法通过自旋 + CAS 实现非公平的信号量获取,步骤如下:

      • 自旋(for(;;) 循环):不断尝试获取信号量,直到成功或确定无法获取;

      • 获取当前可用信号量:通过 getState() 方法获取 Semaphore 中当前可用的信号量数量(available)。getState() 是 AQS 提供的方法,用于维护同步状态(这里同步状态代表可用信号量的数量);

      • 计算剩余信号量remaining = available - acquires,其中 acquires 是线程要获取的信号量数量;

      • CAS 尝试更新状态

        • remaining < 0,说明可用信号量不足,直接返回 remaining(表示获取失败);

        • remaining ≥ 0,则通过 compareAndSetState(available, remaining) 尝试原子性地将“可用信号量”从 available 更新为 remaining。若 CAS 成功,返回 remaining(表示获取成功);若 CAS 失败,说明有其他线程同时修改了信号量状态,继续自旋重试。

3.2 FairSync

  • Semaphore#FairSync#tryAcquireShared():该方法是 FairSync(公平锁实现类)对 AQS 共享式获取逻辑的实现,核心是保证线程“先到先得”的公平性;

    protected int tryAcquireShared(int acquires) {// 自旋for (;;) {// 等待队列中挂起线程,返回-1 (根据返回的-1,将当前线程添加到等待队列中)if (hasQueuedPredecessors())return -1;// 尝试获取Semaphore的信号量,下面与非公平锁逻辑相同int available = getState();int remaining = available - acquires;if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
    }
    
  • 第 5 行的 hasQueuedPredecessors() 是 AQS 提供的方法,用于判断当前线程是否有“前驱节点”(即等待队列中存在比当前线程更早等待的线程)

    • 若返回 true,说明等待队列中已有更早的线程在等待,当前线程直接返回 -1(表示获取失败,会被 AQS 加入等待队列);

    • 这一步是公平锁与非公平锁的核心区别:公平锁会严格检查等待队列的顺序,避免“插队”,而非公平锁则直接尝试抢占;

  • 在通过 hasQueuedPredecessors() 确认“可以尝试抢占”后,后续逻辑与非公平锁类似:

    • 自旋(for(;;) 循环):不断尝试获取信号量,直到成功或确定无法获取;

    • 获取并计算信号量:通过 getState() 获取当前可用信号量(available),再计算获取 acquires 个信号量后的剩余量(remaining = available - acquires);

    • CAS 原子更新:若 remaining ≥ 0,通过 compareAndSetState(available, remaining) 尝试原子性更新信号量状态;若成功则返回 remaining(获取成功),若失败则继续自旋重试;若 remaining < 0,则直接返回 remaining(获取失败)。

4 acquire()

  • Semaphore 默认实现的是非公平锁,下面就按非公平锁的实现进行源码分析;

  • Semaphore#acquire():入口方法

    public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1);
    }
    
    • 作用:尝试获取 1 个信号量,若信号量不足则阻塞线程;支持响应线程中断;

    • 实现:委托给 sync(基于 AQS 的同步组件)的 acquireSharedInterruptibly(1) 方法执行,1 表示要获取的信号量数量;

  • AQS#acquireSharedInterruptibly(int arg):共享式可中断获取逻辑

    public final void acquireSharedInterruptibly(int arg) throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);
    }
    
    • 中断检查if (Thread.interrupted())先检查线程是否被中断,若已中断则抛出 InterruptedException

    • 尝试获取资源:调用 tryAcquireShared(arg)(由 Semaphore 的公平/非公平锁实现,如前所述的 FairSyncNonfairSync 的逻辑)。若返回值 < 0,说明获取失败,进入 doAcquireSharedInterruptibly(arg) 处理阻塞逻辑;

  • AQS#doAcquireSharedInterruptibly(int arg):共享式阻塞获取(可中断)

    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; // 断开原头节点引用,帮助GCfailed = false;return;}}// 若获取失败,判断是否需要挂起线程if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()) // 挂起线程并检查中断throw new InterruptedException();}} finally {if (failed)cancelAcquire(node); // 若获取过程中失败,取消节点的获取请求}
    }
    
    • 加入等待队列addWaiter(Node.SHARED) 将当前线程包装为“共享模式”的节点,加入 AQS 的等待队列尾部;

    • 自旋尝试获取:循环中先判断“前驱是否为头节点”(若为头节点,说明当前节点是队列中最有资格获取资源的线程),然后再次尝试 tryAcquireShared(arg)

    • 线程挂起与中断:若获取失败,通过 shouldParkAfterFailedAcquire 判断是否需要挂起线程;若线程在挂起期间被中断,parkAndCheckInterrupt() 会返回 true,进而抛出 InterruptedException

  • AQS#setHeadAndPropagate(Node node, int propagate):设置头节点并传播唤醒(共享模式关键)

    private void setHeadAndPropagate(Node node, int propagate) {Node h = head;setHead(node); // 将当前节点设为新的头节点// 若剩余资源>0、原头节点状态异常(或为null),则传播唤醒后续共享节点if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {Node s = node.next;if (s == null || s.isShared()) // 若后续节点是共享模式,唤醒它doReleaseShared();}
    }
    
    • 设置头节点setHead(node) 将当前节点标记为新的头节点(头节点代表“已获取资源并执行完毕”的线程);

    • 传播唤醒逻辑:由于 Semaphore 是共享锁,获取资源的线程需要“传播”唤醒后续等待的共享节点。若满足 propagate > 0(剩余资源充足)或队列状态异常等条件,会调用 doReleaseShared() 唤醒后续节点,保证共享资源的并发获取效率。

5 release()

  • Semaphore 默认实现的是非公平锁,下面就按非公平锁的实现进行源码分析;

  • Semaphore#release():入口方法

    public void release() {sync.releaseShared(1);
    }
    
    • 作用:归还 1 个信号量

    • 实现:委托给 sync(基于 AQS 的同步组件)的 releaseShared(1) 方法执行,1 表示要归还的信号量数量;

  • AQS#releaseShared(int arg):共享式释放逻辑

    public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) { // 尝试归还信号量doReleaseShared(); // 唤醒等待队列中的线程return true;}return false;
    }
    
    • 尝试归还资源:调用 tryReleaseShared(arg)(由 Semaphore 实现),若归还成功则进入下一步;

    • 唤醒等待线程:调用 doReleaseShared() 唤醒 AQS 等待队列中阻塞的线程,让它们有机会获取刚归还的信号量;

  • Semaphore#Sync#tryReleaseShared(int releases):信号量归还的核心逻辑

    protected final boolean tryReleaseShared(int releases) {for (;;) { // 自旋int current = getState(); // 获取当前可用信号量(AQS的同步状态)int next = current + releases; // 计算归还后的信号量总数if (next < current) // 防止int溢出(若next为负,说明超出int最大值)throw new Error("Maximum permit count exceeded");if (compareAndSetState(current, next)) // CAS原子更新信号量return true;}
    }
    
    • 自旋 + CAS 保证原子性:通过循环尝试 CAS 操作,确保“归还信号量”的操作是原子的(避免多线程同时归还时的状态冲突);

    • 状态更新getState() 获取当前信号量数量,next = current + releases 计算归还后的数量,再通过 compareAndSetState 原子性更新状态;

  • AQS#doReleaseShared():唤醒等待队列的共享线程

    private void doReleaseShared() {for (;;) { // 自旋Node h = head; // 获取等待队列的头节点if (h != null && h != tail) { // 队列非空且有等待线程int ws = h.waitStatus;if (ws == Node.SIGNAL) { // 头节点状态为SIGNAL(表示后续节点需要唤醒)if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // CAS更新状态失败,继续自旋unparkSuccessor(h); // 唤醒头节点的后继线程} // 处理JDK1.5的bug:将头节点状态设为PROPAGATE,保证共享模式下的唤醒传播else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;}if (h == head) // 头节点未变化,说明唤醒操作完成break;}
    }
    
    • 自旋保证唤醒可靠性:循环处理队列,确保唤醒操作能覆盖所有需要唤醒的线程。

    • 状态判断与唤醒:若头节点状态为 SIGNAL,则通过 unparkSuccessor(h) 唤醒其后继线程;同时处理共享模式下的状态传播(PROPAGATE),保证多个共享线程能依次被唤醒。

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

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

相关文章

Java全栈开发面试实战:从基础到微服务的深度解析

Java全栈开发面试实战&#xff1a;从基础到微服务的深度解析 一、面试开场 面试官&#xff08;中年工程师&#xff0c;穿着休闲但专业&#xff09;&#xff1a;你好&#xff0c;我是李工&#xff0c;今天来聊一下你的技术背景。你之前在XX科技做全栈开发&#xff0c;对吧&#…

CVPR深度学习论文创新合集拆解:模型训练速度算提升

关注gongzhonghao【CVPR顶会精选】大语言模型扩散Transformer的深度融合&#xff0c;让文本到图像生成更精准、细节更丰富&#xff1b;同时&#xff0c;专家轨迹正则化深度强化学习在自动对焦中的稳定加速表现&#xff0c;也展示了深度学习与轨迹建模结合的潜力。这样的组合正在…

【智能体】零代码学习 Coze 智能体(2)创建智能体的完整步骤

欢迎关注【AGI使用教程】 专栏 【智能体】零代码学习 Coze 智能体&#xff08;1&#xff09; 【智能体】零代码学习 Coze 智能体&#xff08;2&#xff09; 【智能体】零代码学习 Coze 智能体&#xff08;1&#xff09;1、登录 Coze 平台2、创建智能体3、智能体编排页面4、编写…

WPF和WinFrom区别

WPF 总结Windows Presentation Foundation (WPF) 是微软开发的一个用于构建 Windows 桌面应用程序的用户界面框架。它基于 .NET Framework&#xff0c;提供丰富的图形、动画和数据绑定功能&#xff0c;帮助开发者创建现代化、高性能的应用程序。以下是其核心要点总结&#xff1…

数据库原理及应用_数据库基础_第3章数据库编程_常用系统函数

前言 "<数据库原理及应用>(MySQL版)".以下称为"本书"中3.1.2节内容 引入 数据库常用系统函数的分析.上一篇帖子分析了,数据库函数需要看看能否被C语言函数替代 1.字符串函数 1)计算字符串字符数的函数和字符串长度的函数 语法: CHAR_LENGTH(str)…

回归问题的损失函数

简单来说&#xff0c;​在回归问题中&#xff0c;最常用的损失函数是均方误差&#xff08;MSE, Mean Squared Error&#xff09;和平均绝对误差&#xff08;MAE, Mean Absolute Error&#xff09;​。它们衡量的都是模型预测值&#xff08;ŷ&#xff09;与真实值&#xff08;y…

吴恩达机器学习(四)

一、神经网络神经元模拟逻辑单元&#xff1a;神经网络简单模型&#xff1a;神经网络中的前向传播过程&#xff1a;依次计算激活项&#xff0c;从输入层到隐藏层再到输出层的过程。样例&#xff1a;多元分类&#xff1a;

【重学 MySQL】九十三、MySQL的字符集的修改与底层原理详解

【重学 MySQL】九十三、MySQL的字符集的修改与底层原理详解一、字符集修改方法1. **配置文件修改**2. **SQL命令修改**3. **数据迁移方案**二、底层原理与注意事项1. **字符集与排序规则**2. **存储与性能影响**3. **数据一致性风险**三、常见问题解决1. **乱码问题**2. **性能…

pdf 转图片工具实现

一、安装 sudo yum install poppler-utils pdftoppm -v pdftoppm -png -r 300 a.pdf /tmp/page 运行效果&#xff1a; PDF转图片工具 - 在线PDF转PNG/JPG/TIFF转换器 | 免费在线工具 后台实现&#xff1a; using System.Diagnostics; using System.IO.Compression;namespac…

Zynq开发实践(FPGA之输入、输出整合)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】fpga开发的时候习惯上先把功能拆分成若干个模块。针对这些模块&#xff0c;一个一、个实现好之后&#xff0c;再用wire连接即可。这一点有点像软件编…

【Linux基础】深入理解计算机启动原理:MBR主引导记录详解

目录 引言 1 硬盘分区初始化概述 1.1 为什么需要硬盘分区 1.2 硬盘分区格式的发展 1.3 分区初始化的基本流程 2 MBR详解 2.1 MBR的定义与位置 2.2 MBR的结构详解 2.3 分区表结构详解 2.4 MBR的工作原理 2.5 MBR的引导程序 3 MBR的局限性 3.1 硬盘容量限制 3.2 分…

Linux 线程同步

线程同步 由于线程共享内存&#xff0c;访问共享数据&#xff08;全局变量、堆内存&#xff09;必须进行同步&#xff0c;以防止竞态条件&#xff08;Race Conditions&#xff09;导致数据不一致或程序崩溃。 子线程没有独立的地址空间&#xff0c;数据通常是共享的&#xff1b…

世界模型的典型框架与分类

1.概述 人类和动物智能的一个重要方面是我们对世界的内部模型。我们使用这个模型来预测我们的行为将如何影响我们的环境&#xff0c;预测未来的事件&#xff0c;并计划复杂的行动序列以实现目标。当前大多数机器学习研究都集中在被动理解数据的模型上&#xff0c;例如图像分类…

【Day 35】Linux-Mysql错误总结

&#xff08;一&#xff09;MySQL 基础操作与服务故障类 连接层错误&#xff08;客户端与服务器的连接建立失败&#xff09; 解决 socket 路径、文件存在性及服务可用性问题。 1、MySQL 客户端连接失败&#xff08;报错 “Cant connect to local MySQL server throgh socket…

MYSQL速通(2/5)

六、多表查询1、多表关系①、一对多&#xff08;多对一&#xff09;举例&#xff1a;一个部门对多个员工实现&#xff1a;多的那边建立外键&#xff0c;指向一的那边的主键②、多对多举例&#xff1a;一个学生可选多门课&#xff0c;一门课可被多个学生选实现&#xff1a;建立中…

CRM、ERP、HRP系统有啥区别?

要理解CRM、ERP、HRP系统&#xff0c;需先明确三者的核心定位&#xff08;聚焦客户、企业全资源、特定领域资源&#xff09;&#xff0c;再从管理范围、目标、用户等维度区分。以下是详细解析&#xff1a; 一、各系统核心定义与核心模块 1. CRM系统&#xff1a;客户关系管理系统…

【系统分析师】高分论文:论系统测试技术及应用

【摘要】 2022 年 7月&#xff0c;我作为项目负贵人&#xff0c;参加了某银行的统计数据发布系统建设项目。该项目合同金额230 万元&#xff0c;合同工期为半年。统计数据发布系统的主要目标是为该行建设一个企业级的数据统计、分析、发布平台&#xff0c;实现定制化的数据应用…

第5篇 c++ 函数的多返回值实现-返回多个值

c 函数的多返回值实现std::tuple<Mat, int, double, std::string> AuatoPafackSydstem::GetMatchingValue(Mat mat_img, std::string img_template_path) {Mat a;return {a,1,0.001,""}; }std::tuple<Mat, int, double, std::string> GetMatchingValue(M…

C++基础(⑤删除链表中的重复节点(链表 + 遍历))

题目描述 给定一个排序好的链表&#xff08;升序&#xff09;&#xff0c;删除所有重复的元素&#xff0c;使每个元素只出现一次。 示例&#xff1a; 输入&#xff1a;1 → 1 → 2 → 3 → 3 输出&#xff1a;1 → 2 → 3 解题思路 核心观察&#xff1a;链表已排序&#xff0c;…

摩搭api 实现

AI图片生成器前端实现详解本文详细解析一个功能完整的AI图片生成器前端实现&#xff0c;包含主题切换、参数配置、图片生成与预览等核心功能。项目概述 这是一个基于ModelScope平台的AI图片生成器前端实现&#xff0c;用户可以通过输入提示词、选择模型和调整参数来生成高质量图…