【JUC】信号量Semaphore详解
欢迎关注专栏【JAVA并发】
欢迎关注个人公众号—— JAVA旭阳
前言
大家应该都用过synchronized 关键字加锁,用来保证某个时刻只允许一个线程运行。那么如果控制某个时刻允许指定数量的线程执行,有什么好的办法呢? 答案就是JUC提供的信号量Semaphore。
介绍和使用
Semaphore(信号量)可以用来限制能同时访问共享资源的线程上限,它内部维护了一个许可的变量,也就是线程许可的数量Semaphore的许可数量如果小于0个,就会阻塞获取,直到有线程释放许可Semaphore是一个非重入锁
API介绍
- 构造方法
public Semaphore(int permits):permits表示许可线程的数量public Semaphore(int permits, boolean fair):fair表示公平性,如果设为true,表示是公平,那么等待最久的线程先执行
- 常用API
public void acquire():表示一个线程获取1个许可,那么线程许可数量相应减少一个public void release():表示释放1个许可,那么线程许可数量相应会增加
- 其他API
void acquire(int permits):表示一个线程获取n个许可,这个数量由参数permits决定void release(int permits):表示一个线程释放n个许可,这个数量由参数permits决定int availablePermits():返回当前信号量线程许可数量int getQueueLength(): 返回等待获取许可的线程数的预估值
基本使用
public static void main(String[] args) {
// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(2);
// 2. 10个线程同时运行
for (int i = 0; i < 8; i++) {
new Thread(() -> {
// 3. 获取许可
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(1);
log.debug("end...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}
运行结果:
原理介绍
上面是Semaphore的类结构图,其中FairSync和NonfairSync是它的内部类,他们共同继承了AQS类,AQS的共享模式提供了Semaphore的加锁、解锁。
如果对AQS不了解的请移步深入浅出理解Java并发AQS的共享锁模式
为了更好的搞懂原理,我们通过一个例子来帮助我们理解。
假设Semaphore 的 permits为 3,这时 5 个线程来获取资源,其中Thread-1,Thread-2,Thread-4CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败。
获取许可acquire()
acquire()主方法会调用sync.acquireSharedInterruptibly(1)方法acquireSharedInterruptibly()方法会先调用tryAcquireShared()方法返回许可的数量,如果小于0个,调用doAcquireSharedInterruptibly()方法进入阻塞
// acquire() -> sync.acquireSharedInterruptibly(1),可中断
public final void acquireSharedInterruptibly(int arg) {
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取通行证,获取成功返回 >= 0的值
if (tryAcquireShared(arg) < 0)
// 获取许可证失败,进入阻塞
doAcquireSharedInterruptibly(arg);
}
tryAcquireShared()方法在终会调用到Sync#nonfairTryAcquireShared()方法nonfairTryAcquireShared()方法中会减去获取的许可数量,返回剩余的许可数量
// tryAcquireShared() -> nonfairTryAcquireShared()
// 非公平,公平锁会在循环内 hasQueuedPredecessors()方法判断阻塞队列是否有临头节点(第二个节点)
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取 state ,state 这里【表示通行证】
int available = getState();
// 计算当前线程获取通行证完成之后,通行证还剩余数量
int remaining = available - acquires;
// 如果许可已经用完, 返回负数, 表示获取失败,
if (remaining < 0 ||
// 许可证足够分配的,如果 cas 重试成功, 返回正数, 表示获取成功
compareAndSetState(available, remaining))
return remaining;
}
}
- 如果剩余的许可数量<0, 会调用
doAcquireSharedInterruptibly()方法将当前线程加入到阻塞队列中阻塞 - 方法中调用
parkAndCheckInterrupt()阻塞当前线程
private void doAcquireSharedInterruptibly(int arg) {
// 将调用 Semaphore.aquire 方法的线程,包装成 node 加入到 AQS 的阻塞队列中
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) {
// 成功后本线程出队(AQS), 所在 Node设置为 head
// r 表示【可用资源数】, 为 0 则不会继续传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
// 被打断后进入该逻辑
if (failed)
cancelAcquire(node);
}
}
最终的AQS状态如下图所示:
Thread-1、Thread-2、Thread-4正常运行- AQS的
state也就是等于0 Thread-0、Thread-3再阻塞队列中
释放许可release()
现在Thread-4运行完毕,要释放许可,Thread-0、Thread-3又是如何恢复执行的呢?
- 调用
release()方法释放许可,最终调用Sync#releaseShared()方法 - 如果方法
tryReleaseShared(arg)尝试释放许可成功,那么调用doReleaseShared();进行唤醒
// release() -> releaseShared()
public final boolean releaseShared(int arg) {
// 尝试释放锁
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared()方法主要是尝试释放许可- 获取当前许可数量 + 释放的数量,然后通过cas设置回去
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 获取当前锁资源的可用许可证数量
int current = getState();
int next = current + releases;
// 索引越界判断
if (next < current)
throw new Error("Maximum permit count exceeded");
// 释放锁
if (compareAndSetState(current, next))
return true;
}
}
- 调用
doReleaseShared()方法唤醒队列中的线程 - 其中
unparkSuccessor()方法是唤醒的核心操作
// 唤醒
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 防止 unparkSuccessor 被多次执行
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
最终AQS状态如下图所示:
- 许可state变回1
- 然后
Thread-0开始竞争,如果竞争成功,如下图所示:
- 由于Thread-0竞争成功,再次获取到许可,许可数量减1,最终又变回0
- 然后等待队列中剩余
Thread-3
总结
Semaphore信号量类基于AQS的共享锁实现,有公平锁和非公平锁两个版本,它用来限制能同时访问共享资源的线程上限,典型的应用场景是可以用来保护有限的公共资源,比如数据库连接等。
如果本文对你有帮助的话,请留下一个赞吧
【JUC】信号量Semaphore详解的更多相关文章
- JAVA Semaphore详解
Semaphore(信号量):是一种计数器,用来保护一个或者多个共享资源的访问.如果线程要访问一个资源就必须先获得信号量.如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源:否则,如果信号量 ...
- Java并发编程系列之Semaphore详解
简单介绍 我们以饭店为例,假设饭店只有三个座位,一开始三个座位都是空的.这时如果同时来了三个客人,服务员人允许他们进去用餐,然后对外说暂无座位.后来的客人必须在门口等待,直到有客人离开.这时,如果有一 ...
- 并发编程Semaphore详解
Semaphore的作用:限制线程并发的数量 位于 java.util.concurrent 下, 构造方法 // 构造函数 代表同一时间,最多允许permits执行acquire() 和releas ...
- ucos事件邮箱信号量队列详解
Ucos的事件分为时钟,信号量,互斥性信号量,消息队列,以及消息邮箱 首先说信号量 信号量在ucos中的类型定义为OS_EVENT_TYPE_SEM,在任务控制块ecb中,主要是用到的是信号量计数器O ...
- Java并发编程的艺术笔记(十)——Semaphore详解
作用:控制同时访问某个特定资源的线程数量,用在流量控制.
- 最强Java并发编程详解:知识点梳理,BAT面试题等
本文原创更多内容可以参考: Java 全栈知识体系.如需转载请说明原处. 知识体系系统性梳理 Java 并发之基础 A. Java进阶 - Java 并发之基础:首先全局的了解并发的知识体系,同时了解 ...
- 跟着阿里p7一起学java高并发 - 第19天:JUC中的Executor框架详解1,全面掌握java并发核心技术
这是java高并发系列第19篇文章. 本文主要内容 介绍Executor框架相关内容 介绍Executor 介绍ExecutorService 介绍线程池ThreadPoolExecutor及案例 介 ...
- java高并发系列 - 第20天:JUC中的Executor框架详解2之ExecutorCompletionService
这是java高并发系列第20篇文章. 本文内容 ExecutorCompletionService出现的背景 介绍CompletionService接口及常用的方法 介绍ExecutorComplet ...
- JUC中的AQS底层详细超详解
摘要:当你使用java实现一个线程同步的对象时,一定会包含一个问题:你该如何保证多个线程访问该对象时,正确地进行阻塞等待,正确地被唤醒? 本文分享自华为云社区<JUC中的AQS底层详细超详解,剖 ...
- Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量
Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量 一丶线程的理论知识 什么是线程: 1.线程是一堆指令,是操作系统调度 ...
随机推荐
- Grafana配置Alert监控告警
1.添加告警途径 这里以slack为例 测试是否可用 在slack上收到告警通知了 安装插件 # grafana-cli plugins install grafana-image-renderer ...
- Android EGL 实践
本项目为 SurfaceView 和 TextureView 封装了 EGL 环境管理以及 Render 线程,可以和 GLSurfaceView 一样使用 OpenGLES 进行渲染.并尝试使用 O ...
- C++ STL 概述_严丝合缝的合作者们
1. 初识 STL 什么是STL? STL(Standard Template Library) 是C++以模板形式提供的一套标准库,提供了很多开发过程需要的通用功能模块.使用 STL ,可以让开发者 ...
- 文件内再分类到各txt文件
当老师叫我们帮他做事,比如文件内内容再分类,我们就可以建个面板,里面有各要导入文件按钮,先把分类内容copy下,再点按钮导入进txt文件就行啦. 以下为java代码,使用了tableLayout布局 ...
- oneplus8手机蓝牙连接tws耳机无法双击退出语音助手
通过蓝牙协议栈我们知道,蓝牙耳机可以通过发送AT指令唤醒或者退出语音助手 唤醒语音助手: AT+BVRA=1 退出语音助手: AT+BVRA=0 但是实际操作中发现双击可以唤醒但再次双击却无法退出语音 ...
- C++和Java多维数组声明和初始化时的区别与常见问题
//C++只有在用{}进行初始化的时候才可以仅仅指定列数而不指定行数,因为可以通过直接//初始化时的元素个数自动计算出行数.而仅声明/创建数组而不初始化时,Cpp要求必须写明//行数和列数才能够创建数 ...
- 9.MongoDB系列之创建副本集(二)
1. 如何设计副本集 大多数:选取主节点时需要由大多数决定,主节点只有在得到大多数支持时才能继续作为主节点,写操作被复制到大多数成员时就是安全的写操作.这里的大多数定义为"副本集中一半以上的 ...
- F118校准(一)-- 安装CA310驱动程序及SDK
1. 准备工作 下载Ca310_drv.zip文件并解压,备用. http://www.xk-image.com/download/blog/0001_F118校准/Ca310_drv.zip 准备好 ...
- 7.pyagem-游戏背景
背景交替滚动 游戏启动后,背景图像不断的向下移动 在视觉上产生角色不断向上移动的错觉 游戏背景不断变化,游戏主角的位置报错不变 实现方案 创建两张背景图 第一张完全和屏幕重合,第二章在屏幕的正上方 ...
- PCA降维的原理及实现
PCA可以将数据从原来的向量空间映射到新的空间中.由于每次选择的都是方差最大的方向,所以往往经过前几个维度的划分后,之后的数据排列都非常紧密了, 我们可以舍弃这些维度从而实现降维 原理 内积 两个向量 ...