Semaphore 源码解读
一、Semaphore
Semaphore 通过设置一个固定数值的信号量,并发时线程通过 acquire() 获取一个信号量,如果能成功获得则可以继续执行,否则将阻塞等待,当某个线程使用 release() 释放一个信号量时,被阻塞的线程则可以被唤醒重新争抢信号量。根据该特征可以有效控制线程的并发数。
那 Semaphore 是如何控制并发的呢,本篇文章带领大家一起解读下 Semaphore 的源码。
在进行源码分析前,先回顾下 Semaphore 是如何使用的,例如下面一个案例:
public class Test {public static void main(String[] args) {Semaphore semaphore = new Semaphore(3);for (int i = 0; i < 10; i++) {new Thread(() -> {try {semaphore.acquire();System.out.println("线程:" + Thread.currentThread().getName() + " 执行, 当前时间:" + LocalDateTime.now().toString());Thread.sleep(1000);} catch (Exception e) {e.printStackTrace();} finally {semaphore.release();}}, String.valueOf(i)).start();}}
}
运行之后,可以看到下面日志:

可以看到每次都是 3 个并发。
在本专栏前面讲解 AQS 源码的时候提到 Semaphore 是基于 AQS 实现的,那是如何使用的 AQS 呢?
在 AQS 中,如果需要使用AQS的特征则需要子类根据使用的场景,重写下面方法:
//查询是否正在独占资源,condition会使用
boolean isHeldExclusively()
//独占模式,尝试获取资源,成功则返回true,失败则返回false
boolean tryAcquire(int arg)
//独占模式,尝试释放资源,成功则返回true,失败则返回false
boolean tryRelease(int arg)
//共享模式,尝试获取资源,如果返回负数表示失败,否则表示成功。
int tryAcquireShared(int arg)
//共享模式,尝试释放资源,成功则返回true,失败则返回false。
boolean tryReleaseShared(int arg)
由于这里 Semaphore 的特性,所以下面我们只需关注共享模式下的几个方法即可。
说明:由于 Semaphore 的实现依赖于 AQS ,因此需要对 AQS 有一定的了解,不了解的小伙伴可以看下这篇对 AQS 源码分析的文章,和当前文章在同一专栏:
https://blog.csdn.net/qq_43692950/article/details/129367736
二、Semaphore 中 Sync、FairSync、NonfairSync
2.1 Sync、FairSync、NonfairSync
在声明 Semaphore 时,有两种方式,一种是使用只有一个 permits 参数的构造函数,一种则需要多增加一个 fair 参数:
new Semaphore(3);
new Semaphore(3, true);
当使用只有一个 permits 参数的构造函数声明时,则是创建了一个 NonfairSync 对象:

通过需要多增加一个 fair 参数的构造函数时,则可以根据传入的 fair 选择创建一个 FairSync 对象:

这里也不难理解 NonfairSync 和 FairSync 其实可以理解为 Semaphore 中的非公平锁和公平锁两种类型。
点到这两个类中,可以看到都继承自 Sync 类:


而 Sync 类,则继承自 AQS :

到这里,我们就可以寻找几个关键的方法,在AQS中共享模式下,两大关键的方法是交由子类进行实现的,分别是 tryAcquireShared 尝试获取资源,和 tryReleaseShared 尝试释放资源。
首先来看 tryAcquireShared 尝试获取资源:
通过 Sync 类的实现源码发现并没有重写 tryAcquireShared方法,那该方法肯定在下面的FairSync 和 NonfairSync 子类中,分别看下源码确实存在重写的方法:


2.2 NonfairSync 下的 tryAcquireShared
这里先分析下 NonfairSync 的 tryAcquireShared 实现逻辑,可以看到又调用了 nonfairTryAcquireShared 就是 Sync 类中的 nonfairTryAcquireShared ,从命名上可以分析出就是非公平锁的尝试获取资源的操作,直观就是非公平锁下获取锁的操作:

进入到 Sync 类中的 nonfairTryAcquireShared 方法中,可以明显看到一个自旋的操作,在循环中首先获取到 AQS 中的共享资源 state ,并对其进行 - acquires (默认为 1 ,后面会进行说明)操作,其实就是 -1 操作,如果减去的值小于 0 或者修改 state 成功,就返回当前减去的值,否则就自旋的方式再次重试:

上一步的操作主要做了什么目的呢,其实从 Sync 的构造方法就可以看出,创建 Semaphore 传递的 permits 参数被赋值给了 AQS 中的 state ,那此时 state 就记录的当前剩余信号量的大小,获取资源就要进行 -1 标识消耗了一个,最后将减去的值返回出去表示剩余的资源,如果信号量小于 0 了,则表示获取资源失败,直观就是获取锁失败。因为在 AQS 中对 tryAcquireShared 方法的判断是小于 0 时,进行线程的入列和挂起等待。


2.3 FairSync下的 tryAcquireShared
在 FairSync 类下的 tryAcquireShared 方法中,和前面 NonfairSync 类似,但不同的是,会首先进行 hasQueuedPredecessors 方法的判断:

下面进到 hasQueuedPredecessors 的方法中,可以看到是由 AQS 提供的方法,主要就是判断当前节点线程的前面是否还有等待的线程,因为 FairSync 实现的是公平锁的原则,如果当前线程前面还有等待线程,则获取锁资源也轮不到自个,让前面的老大先来,所以直接返回 -1 表示获取资源失败:

2.4 tryReleaseShared
到这里已经了解到了 tryAcquireShared尝试获取资源的逻辑,上面提到了两个重要方法,还有一个 tryReleaseShared 没有分析,还是首先看 Sync 类中是否有重写该方法:
通过源码可以看到,在 Sync 类中就已经对 tryReleaseShared 进行了重写,而 NonfairSync 和 FairSync 中都没有重写该方法,那释放资源就是走的 Sync 类下的 tryReleaseShared 方法:

在该方法同样使用了自旋,首先获取到 AQS 中的共享资源 state ,然后进行 + releases (默认情况下为 1 ,后面会说明),其实就是进行 +1 操作,并使用新的值修改 state ,如果修改失败的话则在自旋中继续修改,直到成功后返回 true ,表示释放资源成功。
看到这里就会发现获取资源和释放资源,无非就是对 AQS 中的共享资源 state 进行操作。理解了这两大核心的方法后,下面就可以看如何运用在 Semaphore 中的了。
三、semaphore.acquire()
通过 semaphore.acquire() 可以获取一个信号量,如果获取不到则阻塞等待,那semaphore.acquire() 主要做了什么呢?
下面点到该方法中,可以看到又调用了 sync.acquireSharedInterruptibly 方法,其实就是 AQS 中的 acquireSharedInterruptibly 方法,注意这里传递的参数为 1 ,对应上面括号中的说明:

在 AQS 的 acquireSharedInterruptibly 方法中,首先会使用子类的 tryAcquireShared 方法获取资源,如果资源数小于 0 ,则认为获取失败,下面使用 doAcquireSharedInterruptibly 进行加入队列并挂起阻塞:

关于AQS如何加入队列和挂起,可以参考文章开始的链接中对 AQS 源码的解读。
四、semaphore.release()
上面在获取不到可用的资源时,则会被 AQS 挂起,因此这里还需要进行释放资源。
下面点到 semaphore.release() 方法中,可以看到又调用了 sync.releaseShared ,其实就是 AQS 中的 releaseShared 方法,注意这里参数默认为 1 ,对应上面括号中的说明:

在 AQS 的 releaseShared 方法中,会首先调用子类的 tryReleaseShared 释放资源,释放成功后,会使用 doReleaseShared 进行挂起线程的唤醒:

关于releaseShared方法的源码解读可以参考文章开始的链接中对 AQS 源码的解读。
三、总结
通过阅读 Semaphore的源码可以发现,大量依赖于 AQS 中提供的方法,如果有阅读过本专栏对 ReentrantLock 锁源码的分析,可以发现相似度极高,都是使用 AQS 所提供的的特征实现某些场景的应用。
相关文章:
Semaphore 源码解读
一、Semaphore Semaphore 通过设置一个固定数值的信号量,并发时线程通过 acquire() 获取一个信号量,如果能成功获得则可以继续执行,否则将阻塞等待,当某个线程使用 release() 释放一个信号量时,被阻塞的线程则可以被唤…...
RZ/G2L工业核心板U盘读写速率测试
1. 测试对象HD-G2L-IOT基于HD-G2L-CORE工业级核心板设计,双路千兆网口、双路CAN-bus、2路RS-232、2路RS-485、DSI、LCD、4G/5G、WiFi、CSI摄像头接口等,接口丰富,适用于工业现场应用需求,亦方便用户评估核心板及CPU的性能。HD-G2L…...
《SQL与数据库基础》18. MySQL管理
SQL - MySQL管理MySQL管理系统数据库常用工具mysqlmysqladminmysqlbinlogmysqlshowmysqldumpmysqlimportsource本文以 MySQL 为例 MySQL管理 系统数据库 Mysql数据库安装完成后,自带了以下四个数据库,具体作用如下: 数据库含义mysql存储My…...
达梦关系型数据库
达梦关系型数据库一、DM8 安装1. 安装包下载2. Docker 安装3. Linux 安装4. Windows 安装二、DM 管理工具三、命令行交互工具 DIsql四、DM8 SQL使用1. 创建模式2. 创建表3. 修改表4. 读写数据5. 查看库下所有的表名6. 查看表字段信息GitHub: link. 欢迎star国产自主研发的大型…...
Postgresql | 执行计划
SQL优化主要从三个角度进行: (1)扫描方式; (2)连接方式; (3)连接顺序。 如果解决好这三方面的问题,那么这条SQL的执行效率就基本上是靠谱的。看懂SQL的执行计…...
Vue3之父子组件通过事件通信
前言 组件间传值的章节我们知道父组件给子组件传值的时候,使用v-bind的方式定义一个属性传值,子组件根据这个属性名去接收父组件的值,但是假如子组件想给父组件一些反馈呢?就不能使用这种方式来,而是使用事件的方式&a…...
在云服务器安装tomcat和mysql
将 linux 系统安装包解压到指定目录进入 bin 目录执行./startup.sh 命令启动服务器执行./shutdown.sh 关闭服务器在浏览器中访问虚拟机中的 tomcat ip端口具体操作入下解压tomcat压缩包解压,输入tom按table键自动补全tar -zxvf 启动tomcat进入bin目录在linux启动to…...
IO多路复用(select、poll、epoll网络编程)
目录一、高级IO相关1.1 同步通信和异步通信1.2 阻塞与非阻塞1.3 fcntl 函数二、五种IO模型2.1 阻塞式IO模型2.2 非阻塞式IO模型2.3 多路复用IO模型2.4 信号驱动式IO模型2.5 异步IO模型三、认识IO多路复用四、select4.1 认识select函数4.2 select函数原型4.3 select网络编程4.4 …...
Spark单机伪分布式环境搭建、完全分布式环境搭建、Spark-on-yarn模式搭建
搭建Spark需要先配置好scala环境。三种Spark环境搭建互不关联,都是从零开始搭建。如果将文章中的配置文件修改内容复制粘贴的话,所有配置文件添加的内容后面的注释记得删除,可能会报错。保险一点删除最好。Scala环境搭建上传安装包解压并重命…...
C++网络编程(一)本地socket通信
C网络编程(一) socket通信 前言 本次内容简单描述C网络通信中,采用socket连接客户端与服务器端的方法,以及过程中所涉及的函数概要与部分函数使用细节。记录本人C网络学习的过程。 网络通信的Socket socket,即“插座”,在网络中译作中文“套接字”,应…...
【Docker】Linux下Docker安装使用与Docker-compose的安装
【Docker】的安装与启动 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sudo yum install docker-cesudo systemctl enable dockersudo systemct…...
构造函数与普通函数,显式原型与隐式原型,原型与原型链
原型与原型链1 学前先了解一些概念1.1 构造函数和普通函数的区别1.1.1 调用方式1.1.2 函数中this的指向不同1.1.3 写法不同1.2 问题明确2 原型与原型链2.1 原型2.2 显式原型与隐式原型2.3 原型链3 原型链环形结构1 学前先了解一些概念 1.1 构造函数和普通函数的区别 构造函数…...
跨过社科院与杜兰大学金融管理硕士项目入学门槛,在金融世界里追逐成为更好的自己
没有人不想自己变得更优秀,在职的我们也是一样。当我们摸爬滚打在职场闯出一条路时,庆幸的是我们没有沉浸在当下,而是继续攻读硕士学位,在社科院与杜兰大学金融管理硕士项目汲取能量,在金融世界里追逐成为更好的自己。…...
macOS 13.3 Beta 3 (22E5236f)With OpenCore 0.9.1开发版 and winPE双引导分区原版镜像
原文地址:http://www.imacosx.cn/112494.html(转载请注明出处)镜像特点完全由黑果魏叔官方制作,针对各种机型进行默认配置,让黑苹果安装不再困难。系统镜像设置为双引导分区,全面去除clover引导分区&#x…...
InceptionTime 复现
下载数据集: https://www.cs.ucr.edu/~eamonn/time_series_data/ 挂梯子,开全局模式即可 配置环境 虚拟环境基于python3.9, tensorflow下载:pip install tensorflow,不需要tensorflow-gpu(高版本python&…...
谷粒学院开发(二):教师管理模块
前后端分离开发 前端 html, css, js, jq 主要作用:数据显示 ajax后端 controller service mapper 主要作用:返回数据或操作数据 接口 讲师管理模块(后端) 准备工作 创建数据库,创建讲师数据库表 CREATE TABLE edu…...
2021牛客OI赛前集训营-提高组(第三场) T4扑克
2021牛客OI赛前集训营-提高组(第三场) 题目大意 小A和小B在玩扑克牌游戏,规则如下: 从一副52张牌(没有大小王)的扑克牌中随机发3张到每个玩家手上,每个玩家可以任意想象另外两张牌࿰…...
【OJ比赛日历】快周末了,不来一场比赛吗? #03.11-03.17 #12场
CompHub 实时聚合多平台的数据类(Kaggle、天池…)和OJ类(Leetcode、牛客…)比赛。本账号同时会推送最新的比赛消息,欢迎关注!更多比赛信息见 CompHub主页 或 点击文末阅读原文以下信息仅供参考,以比赛官网为准目录2023-03-11&…...
C++-说一说异常机制
C异常机制是一种处理程序错误的高级方法。当程序出现错误时,可以通过抛出异常来通知调用者进行处理,或者在异常对象被捕获之后终止程序执行。 异常处理语法 在C中,可以使用 throw 抛出异常, try-catch 处理异常,try块中…...
k8s CSI插件浅析
Kubernetes CSI (Container Storage Interface)插件是一种可插拔的存储插件,可以将外部存储系统的功能集成到Kubernetes集群中。它允许Kubernetes管理员动态地将外部存储系统映射到容器中,以满足应用程序对持久化存储的需求。 CSI插件基于一组规范定义的…...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...
虚拟电厂发展三大趋势:市场化、技术主导、车网互联
市场化:从政策驱动到多元盈利 政策全面赋能 2025年4月,国家发改委、能源局发布《关于加快推进虚拟电厂发展的指导意见》,首次明确虚拟电厂为“独立市场主体”,提出硬性目标:2027年全国调节能力≥2000万千瓦࿰…...
从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践
作者:吴岐诗,杭银消费金融大数据应用开发工程师 本文整理自杭银消费金融大数据应用开发工程师在StarRocks Summit Asia 2024的分享 引言:融合数据湖与数仓的创新之路 在数字金融时代,数据已成为金融机构的核心竞争力。杭银消费金…...
零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...
数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)
名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 原创笔记:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:《数据结构第4章 数组和广义表》…...
Mysql故障排插与环境优化
前置知识点 最上层是一些客户端和连接服务,包含本 sock 通信和大多数jiyukehuduan/服务端工具实现的TCP/IP通信。主要完成一些简介处理、授权认证、及相关的安全方案等。在该层上引入了线程池的概念,为通过安全认证接入的客户端提供线程。同样在该层上可…...
