【Java并发编程】信号量Semaphore详解

一、简介

Semaphore(信号量):是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

Semaphore 一般用于流量的控制,特别是公共资源有限的应用场景。例如数据库的连接,假设数据库的连接数上线为10个,多个线程并发操作数据库可以使用Semaphore来控制并发操作数据库的线程个数最多为10个。

Semaphore 是一个有效的流量控制工具,它基于 AQS 共享锁实现。我们常常用它来控制对有限资源的访问。

  • 每次使用资源前,先申请一个信号量,如果资源数不够,就会阻塞等待;
  • 每次释放资源后,就释放一个信号量。

二、源码

2.1 类总览

通过上面的类图可以看到,SemaphoreReentrantLock 的内部类的结构相同,类内部总共存在 SyncNonfairSyncFairSync 三个类, NonfairSync 与 FairSync 类继承自 Sync 类,其只有一个 tryAcquireShared() 方法,重写了AQS的该方法。Sync 类继承自 AbstractQueuedSynchronizer 抽象类。

CountDownLatch 类似,Semaphore 主要是通过 AQS 的共享锁机制实现的,因此它的核心属性只有一个 Sync。总体源码如下:

public class Semaphore implements java.io.Serializable {//序列化版本号private static final long serialVersionUID = -3222578661600680210L;//同步队列private final Sync sync;//构造方法//指定许可数,默认为非公平策略public Semaphore(int permits) {sync = new NonfairSync(permits);}//指定许可数和是否公平策略public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);}//Semaphore提供了acquire方法来获取一个许可,会阻塞线程(有重载方法,可以指定获取许可的个数)public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1); //调用AQS的acquireSharedInterruptibly方法, 即共享式获取响应中断}//tryAcquire的意思是尝试获取许可,如果获取成功返回true,否则返回false,不会阻塞线程,而且不响应中断public boolean tryAcquire() {return sync.nonfairTryAcquireShared(1) >= 0;}//Semaphore提供release来释放许可public void release() {sync.releaseShared(1); //调用AQS的releaseShared方法,即释放共享式同步状态}abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 1192457210091910933L;Sync(int permits) {setState(permits);}//获取许可数目    final int getPermits() {return getState();}//共享模式下非公平策略获取//本质就是一个自旋方法,通过自旋+CAS来保证修改许可值的线程安全性,该方法返回的情况有如下两种情况://   信号量不够,直接返回,返回值为负数,表示获取失败;//   信号量足够,且CAS操作成功,返回值为剩余许可值,获取成功。final int nonfairTryAcquireShared(int acquires) {for (;;) { //自旋int available = getState(); //获取可用许可值int remaining = available - acquires; //计算剩余的许可值//如果剩余许可值小于0,说明许可不够用了,直接返回,否则CAS更新许可值,更新成功返回,否则继续自旋if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}//共享模式下进行释放//该方法也是一个自旋方法,通过自旋+CAS原子性地修改许可值protected final boolean tryReleaseShared(int releases) {for (;;) { //自旋int current = getState(); //获取许可值int next = current + releases; //计算释放后的许可值if (next < current) // overflowthrow new Error("Maximum permit count exceeded");if (compareAndSetState(current, next)) //CAS修改许可值,成功则返回,失败则继续自旋return true;}}//根据指定的缩减量减小可用许可的数目final void reducePermits(int reductions) {for (;;) {int current = getState();int next = current - reductions;if (next > current) // underflowthrow new Error("Permit count underflow");if (compareAndSetState(current, next))return;}}//获取并返回立即可用的所有许可数目final int drainPermits() {for (;;) {int current = getState();if (current == 0 || compareAndSetState(current, 0))return current;}}}//采用非公平策略获取资源static final class NonfairSync extends Sync {private static final long serialVersionUID = -2694183684443567898L;NonfairSync(int permits) {super(permits);}//获取许可protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires); //共享模式下非公平策略获取}}//采用公平策略获取资源static final class FairSync extends Sync {private static final long serialVersionUID = 2014338818796000944L;FairSync(int permits) {super(permits);}//获取许可protected int tryAcquireShared(int acquires) {for (;;) {//获取共享锁之前,先调用hasQueuedPredecessors方法来判断队列中是否存在其他正在排队的节点,// 如果是返回true,否则为false。因此当存在其他正在排队的节点,当前节点就无法获取许可,只能排队等待,这也是公平策略的体现。if (hasQueuedPredecessors())return -1;int available = getState();int remaining = available - acquires;if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}}
}

2.2 核心方法

获取信号量的方法总共有四个:

释放信号量的方法有两个:

获取信号量四个方法中后面三个方法原理同 acquire() ,我们这里来分析一下 acquire() 和 release() 方法。

2.2.1 acquire() 方法

获取许可,会阻塞线程,响应中断。

// Semaphore
public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1);
}

内部调用的是 AQSacquireSharedInterruptibly() 方法, 即共享式获取响应中断,代码如下:

// AbstractQueuedSynchronizer
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);
}

除了 tryAcquireShared() 方法由 AQS 子类实现,其他方法在 《AQS实现原理》中有讲解过,这里不再赘述。我们来分析一下子类实现的 tryAcquireShared() 方法,这里就要分公平和非公平策略两种情况了。

2.2.1.1 非公平策略下

非公平策略下的 tryAcquireShared() 方法:

// Semaphore#NonfairSync
protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);
}

内部调用 Sync#nonfairTryAcquireShared() 方法:

// Sync
final int nonfairTryAcquireShared(int acquires) {//自旋for (;;) {//获取可用许可值int available = getState();//计算剩余的许可值int remaining = available - acquires;//如果剩余许可值小于0,说明许可不够用了,直接返回,否则CAS更新同步状态,更新成功返回,否则继续自旋if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
}

该方法本质就是一个自旋方法,通过自旋+CAS来保证修改许可值的线程安全性。方法返回的情况有如下两种情况

  • 信号量不够,直接返回,返回值为负数,表示获取失败;
  • 信号量足够,且CAS操作成功,返回值为剩余许可值,获取成功。

2.2.1.2 公平策略下

公平策略下的 tryAcquireShared() 方法如下:

// Semaphore#FairSync
protected int tryAcquireShared(int acquires) {//自旋for (;;) {if (hasQueuedPredecessors())return -1;int available = getState();int remaining = available - acquires;if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
}

我们看到它与非公平策略的唯一区别就是多了下面这个 if 代码:

protected int tryAcquireShared(int acquires) {for (;;) {if (hasQueuedPredecessors())return -1;......}
}// AbstractQueuedSynchronizer
public final boolean hasQueuedPredecessors() {// The correctness of this depends on head being initialized// before tail and on head.next being accurate if the current// thread is first in queue.Node t = tail; // Read fields in reverse initialization orderNode h = head;Node s;return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}

即在获取共享锁之前,先调用 hasQueuedPredecessors() 方法来判断队列中是否存在其他正在排队的节点,如果是返回true,否则为false。因此当存在其他正在排队的节点,当前节点就无法获取许可,只能排队等待,这也是公平策略的体现。

2.2.2 release() 方法

Semaphore 提供 release() 方法来释放许可。我们继续分析 release() 方法,源码如下:

// Semaphore
public void release() {sync.releaseShared(1);
}//AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {//如果释放锁成功,唤醒正在排队的节点doReleaseShared();return true;}return false;
}//Semaphore#Sync
protected final boolean tryReleaseShared(int releases) {//自旋for (;;) {//获取许可值int current = getState();//计算释放后的许可值int next = current + releases;//如果释放后比释放前的许可值还小,直接报Errorif (next < current) // overflowthrow new Error("Maximum permit count exceeded");//CAS修改许可值,成功则返回,失败则继续自旋if (compareAndSetState(current, next))return true;}
}

tryReleaseShared() 方法是一个自旋方法,通过自旋+CAS原子性地修改同步状态,逻辑很简单。

2.2.3 其余方法

获取信号量的方法有四个:

释放信号量的方法有两个:

其余获取和释放信号量的方法原理同上问,不再赘述。接下来看看其余的工具方法。

2.2.3.1 tryAcquire() 尝试获取许可

该方法一共有四种重载形式:

  • tryAcquire() :尝试获取许可,如果获取成功返回true,否则返回false,不会阻塞线程,而且不响应中断。
  • tryAcquire(int permits) :同上的基础上,可以指定获取许可的个数。
  • tryAcquire(long timeout, TimeUnit unit) :指定超时时间,它调用AQS的tryAcquireSharedNanos() 方法,即共享式超时获取。
  • tryAcquire(int permits, long timeout, TimeUnit unit) :可以指定获取许可的个数和超时时间。
//Semaphore
public boolean tryAcquire() {return sync.nonfairTryAcquireShared(1) >= 0;
}public boolean tryAcquire(int permits) {if (permits < 0) throw new IllegalArgumentException();return sync.nonfairTryAcquireShared(permits) >= 0;
}public boolean tryAcquire(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}public boolean tryAcquire(int permits, long timeout, TimeUnit unit)throws InterruptedException {if (permits < 0) throw new IllegalArgumentException();return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
2.2.3.2 availablePermits() 获取可用许可数

源码如下:

//Semaphore
public int availablePermits() {//获取可用许可数return sync.getPermits();
}//Sync
//获取可用许可数
final int getPermits() {return getState();
}

2.2.3.3 drainPermits() 耗光信号量

将剩下的信号量一次性消耗光,并且返回所消耗的信号量。

//Semaphore
public int drainPermits() {return sync.drainPermits();
}//Sync
final int drainPermits() {//自旋操作for (;;) {//获取信号量值int current = getState();//如果信号量为0,直接返回//否则CAS修改为0,成功则返回,否则继续自旋if (current == 0 || compareAndSetState(current, 0))return current;}
}
2.2.3.4 reducePermits() 减少信号量

reducePermits() 和 acquire() 方法相比都是减少信号量的值,但是 reducePermits() 不会导致任何线程阻塞,即只要传递的参数 reductions(减少的信号量的数量)大于0,操作就会成功。所以调用该方法可能会导致信号量最终为负数

//Semaphore
protected void reducePermits(int reduction) {if (reduction < 0) throw new IllegalArgumentException();sync.reducePermits(reduction);
}//Sync
final void reducePermits(int reductions) {//自旋for (;;) {//获取当前信号量值int current = getState();//计算剩余许可值int next = current - reductions;if (next > current) // underflowthrow new Error("Permit count underflow");//CAS修改同步状态,成功则返回,失败则继续自旋if (compareAndSetState(current, next))return;}
}

三、使用案例

这里以经典的停车作为案例。假设停车场有3个停车位,此时有5辆汽车需要进入停车场停车。

public static void main(String[] args) {//定义semaphore实例,设置许可数为3,即停车位为3个Semaphore semaphore = new Semaphore(3);//创建五个线程,即有5辆汽车准备进入停车场停车for (int i = 1; i <= 5; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + "尝试进入停车场...");//尝试获取许可semaphore.acquire();//模拟停车long time = (long) (Math.random() * 10 + 1);System.out.println(Thread.currentThread().getName() + "进入了停车场,停车" + time +"秒...");Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println(Thread.currentThread().getName() + "开始驶离停车场...");//释放许可semaphore.release();System.out.println(Thread.currentThread().getName() + "离开了停车场!");}}, i + "号汽车").start();}
}
//执行结果
1号汽车尝试进入停车场...
5号汽车尝试进入停车场...
4号汽车尝试进入停车场...
3号汽车尝试进入停车场...
2号汽车尝试进入停车场...
5号汽车进入了停车场,停车5秒...
1号汽车进入了停车场,停车8秒...
4号汽车进入了停车场,停车9秒...
5号汽车开始驶离停车场...
5号汽车离开了停车场!
3号汽车进入了停车场,停车10秒...
1号汽车开始驶离停车场...
1号汽车离开了停车场!
2号汽车进入了停车场,停车2秒...
4号汽车开始驶离停车场...
4号汽车离开了停车场!
2号汽车开始驶离停车场...
2号汽车离开了停车场!
3号汽车开始驶离停车场...
3号汽车离开了停车场!

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

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

相关文章

Spring Boot框架下租房管理系统的设计与实现

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

9.Linux按键驱动-工作队列

1.思路 1.1在gpio结构体中定义工作队列 1.2 在probe函数中初始化工作队列 1.3.在中断服务程序中调度工作队列 1.4工作队列处理函数&#xff1a; 2.编程 程序&#xff1a; #include <linux/module.h> #include <linux/fs.h> #include <linux/errno.h> #…

基于微信小程序实现信阳毛尖茶叶商城系统设计与实现

作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验&#xff0c;被多个学校常年聘为校外企业导师&#xff0c;指导学生毕业设计并参与学生毕业答辩指导&#xff0c;…

xtu 哈希

样例输入# 2 2 1 2 4 1 3 5 7 样例输出# 2 3解题思路&#xff1a;欧拉筛素数 满足题意的素数值一定是从n/2开始的。原因&#xff1a;因为每个哈希函数值最多冲突一次&#xff0c;也就是一个哈希值最多出现2次&#xff0c;最坏的情况&#xff0c;数组中所有哈希值都出现2次&…

【判断推理】逻辑论证之一般质疑

不明确选项&#xff1a;表述不够明确&#xff0c;属于既不能加强、也不能削弱的选项 5.1 无论据有结论的一般质疑 无论据有结论&#xff1a;题干往往由“背景、分析、结论”&#xff0c;用提出反向论据的方式反驳论点。 这类题挺简单的&#xff0c;注意话题一致、主题正确&am…

手机拍证件照,换正装有领衣服及底色的方法

证件照在我们的职业生涯的关键节点是经常会用到的&#xff0c;比如毕业入职、人事档案建立、升迁履历、执业资格考试和领证等&#xff0c;这些重要的证件照往往要求使用正装照&#xff0c;有时候手头没有合适的衣服&#xff0c;或者原先的证件照背景色不符合要求&#xff0c;就…

【Fargo】18:camera获取及预览

QCameraViewfinder.ui Build started at 0:10... 1>------ Build started: Project: Fargo, Configuration: Debug Win32 ------

acaconda基础指令(实用不罗嗦,持续更新)

查看conda版本 conda --version 更新acaconda conda update Anaconda 查看有哪些虚拟环境&#xff08;三条均可&#xff09; conda env listconda info -econda info --envs 创建虚拟环境 conda create -n env_name python3.8 激活虚拟环境 conda activate env_name …

Python图像处理——基于ResNet152的人脸识别签到系统(Pytorch框架)

&#xff08;1&#xff09;数据集制作 本次使用明星做为数据集&#xff0c;首先编写爬虫函数&#xff0c;根据关键字爬取对应的明星&#xff0c;爬取结果保存至data文件夹&#xff0c;并以标签名作为文件名。具体爬取的明星如下&#xff1a; 注&#xff1a;实际应用中&#xf…

【thinkphp8】00005 thinkphp8 Db::table和Db::name的区别

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【th…

FreeRTOS 6:任务创建函数xTaskCreate分析

动态创建任务xTaskCreate xTaskCreate 1、申请堆栈内存&#xff08;返回首地址&#xff09; 2、申请任务控制块内存&#xff08;返回首地址&#xff09; 3、把前面申请的堆栈地址&#xff0c;赋值给控制块的堆栈成员 4、调用prvlnitialiseNewTask初始化任务控制块中的成员 …

linux系统安全:开源的反病毒工具ClamAV的安装配置使用和维护介绍

目录 一、概述 1、开发者 2、功能 3、特性 二、安装ClamAV 1、更新系统包列表 2、安装ClamAV 三、更新病毒库 四、配置ClamAV 1、编辑ClamAV配置文件 2、主要配置选项 &#xff08;1&#xff09;/etc/clamd.conf &#xff08;2&#xff09; /etc/freshclam.conf …

windows下安装python库wordCloud报错

换电脑安装wordcloud半天安装失败&#xff0c;记录一下遇到的坑&#xff0c;也给大家节省点时间。 方法1&#xff1a; 错误呢就是下面这个&#xff0c;说没c编译器&#xff0c;要不就去他给的地址上安装一下&#xff0c;我安装了一下好像没什么选&#xff0c;也没太敢勾选吗&a…

2025 - AI人工智能药物设计 - 中药网络药理学和毒理学的研究

中药网络药理学和毒理学的研究 TCMSP&#xff1a;https://old.tcmsp-e.com/tcmsp.php 然后去pubchem选择&#xff1a;输入Molecule Name 然后进行匹配&#xff1a;得到了smiles 再次通过smiles&#xff1a;COC1C(CC(C2C1OC(CC2O)C3CCCCC3)O)O 然后再次输入&#xff1a;http…

C语言基础(五)【控制语句与循环(上)学习篇】

文章目录 前言一、if - else 语句二、switch-case 语句三、while 循环四、for 循环五、do - while 循环六、break 语句七、continue 语句八、go to 语句总结 前言 C语言是结构化的程序设计语言&#xff0c;这里的结构指的是顺序结构、选择结构、循环结构&#xff0c;因此提供了…

中国人寿财险青岛市分公司:科技赋能,车险服务再升级

中国人寿财险青岛市分公司积极响应国家号召&#xff0c;大力推进车险综合改革&#xff0c;以科技赋能车险服务&#xff0c;实现服务再升级。公司利用AI技术&#xff0c;实现车险报案的自动化处理&#xff0c;大幅缩短了理赔周期。同时&#xff0c;公司还通过大数据分析&#xf…

【力扣】[Java版] 刷题笔记-101. 对称二叉树

题目&#xff1a;101. 对称二叉树 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 解题思路 可以理解为遍历对比&#xff0c;最简单的方法就是递归。 解题过程 递归&#xff1a;左右子树分开遍历&#xff0c;左子树遵循根、左、右的顺序&#xff0c;右子树循…

重学SpringBoot3-Spring WebFlux之SSE服务器发送事件

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ Spring WebFlux之SSE服务器发送事件 1. 什么是 SSE&#xff1f;2. Spring Boot 3 响应式编程与 SSE为什么选择响应式编程实现 SSE&#xff1f; 3. 实现 SSE 的基本步骤3.…

(三)第一个Qt程序“Qt版本的HelloWorld”

一、随记 我们在学习编程语言的时候&#xff0c;各种讲解编程语言的书籍中通常都会以一个非常经典的“HelloWorld”程序展开详细讲解。程序虽然简短&#xff0c;但是“麻雀虽小&#xff0c;五脏俱全”&#xff0c;但是却非常适合用来熟悉程序结构、规范&#xff0c;快速形成对编…

技术杂谈与进阶01--------战时操作系统与国产化数据库 |截止到目前修改时间,本文已上全站综合热榜

写文不易 给我点点关注 和点点赞 点点收藏吧 目录 为什么国产化&#xff1f; 操作系统 suse 统信uos 的阉割版 deepin &#xff08;deepin又名深度操作系统&#xff09; 麒麟 的阉割版 -欧拉 debian 的修改残版 ubuntu 国产数据库 开源数据库 部分开源数据库 …