欢迎关注专栏【JAVA并发】

欢迎关注个人公众号—— JAVA旭阳

前言

大家应该都用过synchronized 关键字加锁,用来保证某个时刻只允许一个线程运行。那么如果控制某个时刻允许指定数量的线程执行,有什么好的办法呢? 答案就是JUC提供的信号量Semaphore

介绍和使用

  • Semaphore(信号量)可以用来限制能同时访问共享资源的线程上限,它内部维护了一个许可的变量,也就是线程许可的数量
  • Semaphore的许可数量如果小于0个,就会阻塞获取,直到有线程释放许可
  • Semaphore是一个非重入锁

API介绍

  1. 构造方法
  • public Semaphore(int permits)permits 表示许可线程的数量
  • public Semaphore(int permits, boolean fair)fair 表示公平性,如果设为 true,表示是公平,那么等待最久的线程先执行
  1. 常用API
  • public void acquire():表示一个线程获取1个许可,那么线程许可数量相应减少一个
  • public void release():表示释放1个许可,那么线程许可数量相应会增加
  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的类结构图,其中FairSyncNonfairSync是它的内部类,他们共同继承了AQS类,AQS的共享模式提供了Semaphore的加锁、解锁。

如果对AQS不了解的请移步深入浅出理解Java并发AQS的共享锁模式

为了更好的搞懂原理,我们通过一个例子来帮助我们理解。

假设Semaphorepermits为 3,这时 5 个线程来获取资源,其中Thread-1Thread-2Thread-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-1Thread-2Thread-4正常运行
  • AQS的state也就是等于0
  • Thread-0Thread-3再阻塞队列中

释放许可release()

现在Thread-4运行完毕,要释放许可,Thread-0Thread-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详解的更多相关文章

  1. JAVA Semaphore详解

    Semaphore(信号量):是一种计数器,用来保护一个或者多个共享资源的访问.如果线程要访问一个资源就必须先获得信号量.如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源:否则,如果信号量 ...

  2. Java并发编程系列之Semaphore详解

    简单介绍 我们以饭店为例,假设饭店只有三个座位,一开始三个座位都是空的.这时如果同时来了三个客人,服务员人允许他们进去用餐,然后对外说暂无座位.后来的客人必须在门口等待,直到有客人离开.这时,如果有一 ...

  3. 并发编程Semaphore详解

    Semaphore的作用:限制线程并发的数量 位于 java.util.concurrent 下, 构造方法 // 构造函数 代表同一时间,最多允许permits执行acquire() 和releas ...

  4. ucos事件邮箱信号量队列详解

    Ucos的事件分为时钟,信号量,互斥性信号量,消息队列,以及消息邮箱 首先说信号量 信号量在ucos中的类型定义为OS_EVENT_TYPE_SEM,在任务控制块ecb中,主要是用到的是信号量计数器O ...

  5. Java并发编程的艺术笔记(十)——Semaphore详解

    作用:控制同时访问某个特定资源的线程数量,用在流量控制.

  6. 最强Java并发编程详解:知识点梳理,BAT面试题等

    本文原创更多内容可以参考: Java 全栈知识体系.如需转载请说明原处. 知识体系系统性梳理 Java 并发之基础 A. Java进阶 - Java 并发之基础:首先全局的了解并发的知识体系,同时了解 ...

  7. 跟着阿里p7一起学java高并发 - 第19天:JUC中的Executor框架详解1,全面掌握java并发核心技术

    这是java高并发系列第19篇文章. 本文主要内容 介绍Executor框架相关内容 介绍Executor 介绍ExecutorService 介绍线程池ThreadPoolExecutor及案例 介 ...

  8. java高并发系列 - 第20天:JUC中的Executor框架详解2之ExecutorCompletionService

    这是java高并发系列第20篇文章. 本文内容 ExecutorCompletionService出现的背景 介绍CompletionService接口及常用的方法 介绍ExecutorComplet ...

  9. JUC中的AQS底层详细超详解

    摘要:当你使用java实现一个线程同步的对象时,一定会包含一个问题:你该如何保证多个线程访问该对象时,正确地进行阻塞等待,正确地被唤醒? 本文分享自华为云社区<JUC中的AQS底层详细超详解,剖 ...

  10. Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量

    Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量 一丶线程的理论知识 什么是线程:    1.线程是一堆指令,是操作系统调度 ...

随机推荐

  1. Grafana配置Alert监控告警

    1.添加告警途径 这里以slack为例 测试是否可用 在slack上收到告警通知了 安装插件 # grafana-cli plugins install grafana-image-renderer ...

  2. Android EGL 实践

    本项目为 SurfaceView 和 TextureView 封装了 EGL 环境管理以及 Render 线程,可以和 GLSurfaceView 一样使用 OpenGLES 进行渲染.并尝试使用 O ...

  3. C++ STL 概述_严丝合缝的合作者们

    1. 初识 STL 什么是STL? STL(Standard Template Library) 是C++以模板形式提供的一套标准库,提供了很多开发过程需要的通用功能模块.使用 STL ,可以让开发者 ...

  4. 文件内再分类到各txt文件

    当老师叫我们帮他做事,比如文件内内容再分类,我们就可以建个面板,里面有各要导入文件按钮,先把分类内容copy下,再点按钮导入进txt文件就行啦. 以下为java代码,使用了tableLayout布局 ...

  5. oneplus8手机蓝牙连接tws耳机无法双击退出语音助手

    通过蓝牙协议栈我们知道,蓝牙耳机可以通过发送AT指令唤醒或者退出语音助手 唤醒语音助手: AT+BVRA=1 退出语音助手: AT+BVRA=0 但是实际操作中发现双击可以唤醒但再次双击却无法退出语音 ...

  6. C++和Java多维数组声明和初始化时的区别与常见问题

    //C++只有在用{}进行初始化的时候才可以仅仅指定列数而不指定行数,因为可以通过直接//初始化时的元素个数自动计算出行数.而仅声明/创建数组而不初始化时,Cpp要求必须写明//行数和列数才能够创建数 ...

  7. 9.MongoDB系列之创建副本集(二)

    1. 如何设计副本集 大多数:选取主节点时需要由大多数决定,主节点只有在得到大多数支持时才能继续作为主节点,写操作被复制到大多数成员时就是安全的写操作.这里的大多数定义为"副本集中一半以上的 ...

  8. F118校准(一)-- 安装CA310驱动程序及SDK

    1. 准备工作 下载Ca310_drv.zip文件并解压,备用. http://www.xk-image.com/download/blog/0001_F118校准/Ca310_drv.zip 准备好 ...

  9. 7.pyagem-游戏背景

    背景交替滚动 游戏启动后,背景图像不断的向下移动 在视觉上产生角色不断向上移动的错觉 游戏背景不断变化,游戏主角的位置报错不变   实现方案 创建两张背景图 第一张完全和屏幕重合,第二章在屏幕的正上方 ...

  10. PCA降维的原理及实现

    PCA可以将数据从原来的向量空间映射到新的空间中.由于每次选择的都是方差最大的方向,所以往往经过前几个维度的划分后,之后的数据排列都非常紧密了, 我们可以舍弃这些维度从而实现降维 原理 内积 两个向量 ...