Semaphore信号量源码解析
介绍
Semaphore是什么
Semaphore
可以称为信号量,这个原本是操作系统中的概念,是一种线程同步方法,配合PV操作实现线程之间的同步功能。信号量可以表示操作系统中某种资源的个数,因此可以用来控制同时访问该资源的最大线程数,以保证资源的合理使用
Java的JUC对Semaphore
做了具体的实现,其功能和操作系统中的信号量基本一致,也是一种线程同步并发工具
使用场景
Semaphore
常用于某些有限资源的并发使用场景,即限流
场景一
数据库连接池对于同时连接的线程数有限制,当连接数达到限制后,接下来的线程必须等待前面的线程释放连接才可以获得数据库连接
场景二
医院叫号,放出的号数是固定的,不然医院窗口来不及处理。因此只有取到号才能去门诊,没取到号的只能在外面等待放号
Semaphore常用方法介绍
构造函数
Semaphore(int permits)
:创建一个许可数为permits
的Semaphore
Semaphore(int permits, boolean fair)
:创建一个许可数为permits
的Semaphore
,可以选择公平模式或非公平模式
获取许可
void acquire() throws InterruptedException
:获取一个许可,响应中断,获取不到则阻塞等待void acquire(int permits) throws InterruptedException
:获取指定数量的许可,响应中断,获取不到则阻塞等待void acquireUninterruptibly()
:获取一个许可,忽略中断,获取不到则阻塞等待void acquireUninterruptibly(int permits)
:获取指定数量的许可,忽略中断,获取不到则阻塞等待int drainPermits()
:获取当前所有可用的许可,并返回获得的许可数
释放许可
void release()
:释放一个许可void release(int permits)
:释放指定数量的许可
尝试获取许可
boolean tryAcquire()
:尝试获取一个许可,如果获取失败不会被阻塞,而是返回false。成功则返回trueboolean tryAcquire(int permits)
:尝试获取指定数量的许可,如果获取失败不会被阻塞,而是返回false。成功则返回trueboolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException
:尝试获取一个许可,如果获取失败则会等待指定时间,如果超时还未获得,则返回false。获取成功则返回true。在等待过程中如果被中断,则会立即抛出中断异常boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException
:尝试获取指定数量的许可,如果获取失败则会等待指定时间,如果超时还未获得,则返回false。获取成功则返回true。在等待过程中如果被中断,则会立即抛出中断异常
其他方法
boolean isFair()
:判断信号量是否是公平模式int availablePermits()
:返回当前可用的许可数boolean hasQueuedThreads()
:判断是否有线程正在等待获取许可int getQueueLength()
:返回当前等待获取许可的线程的估计值。该值并不是一个确定值,因为在执行该函数时,线程数可能已经发生了变化
Semaphore使用示例
针对“使用场景”的场景二,假设医院最多接待2个人,如果接待满了,那么所有人都必须在大厅等待(不能“忙等”)
代码如下:
无论如何,医院内最多有5个病人同时接诊。这也说明了Semaphore
可以控制某种资源最多同时被指定数量的线程使用
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
构造函数
Semaphore
有两个构造函数,都是需要设置许可总数,额外的另一个参数是用来控制公平模式or非公平模式的,如果不设置(默认)或设为false,则是非公平模式。如果设置true,则是公平模式
两种构造函数的源码如下:
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);
}
公平模式和非公平模式的区别,就是在于sync
域是FairSync
类还是NonfairSync
类,这两种子类分别对应这两种模式。而permits
参数代表许可的个数,作为这两个类的构造函数参数传入,源码如下:
FairSync(int permits) {
super(permits);
}
NonfairSync(int permits) {
super(permits);
}
这两个类的构造函数都是进一步调用父类Sync
的构造函数:
Sync(int permits) {
setState(permits);
}
从这里就可以明白,许可个数就是state
的值。这里为AQS的state
域赋了初值,为permits
重要结论:在Semaphore
的语境中,AQS的state
就表示许可的个数!对于Semaphore
的任何获取、释放许可操作,本质上都是state
的增减操作
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
获取许可
响应中断的获取
acquire
是响应中断的获取许可方法,有两个重载版本。如果获取成功,则返回;如果许可不够而导致获取失败,则会进入AQS的同步队列阻塞等待。在整个过程中如果被中断,则会抛出中断异常
acquire(int)
首先来看看可以获取指定数量许可的acquire
方法,其源码如下:
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
该方法会先检查permits
是否合法(非负数),再将后续的执行过程委托给Sync
类的父类AQS的acquireSharedInterruptibly
方法来执行,其源码如下:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
该方法响应中断,首先检查中断状态,如果已经被中断则会抛出中断异常。接下来调用钩子方法tryAcquireShared
。如果返回值小于0,说明获取许可失败,会调用doAcquireSharedInterruptibly
进入同步队列阻塞等待,等待过程中响应中断
tryAcquireShared
由Sync
的两个子类实现,分别对应公平模式、非公平模式下的获取。这里由于许可是共享资源,所以使用到的AQS的钩子方法都是针对共享资源的获取、释放的。这也很合理,因为许可是可以被多个线程同时持有的,所以Semaphore
中的许可是一种共享资源!
接下来分别看一看公平模式和非公平模式下,tryAcquireShared
的实现方式是怎样的
公平模式
FairSync
类实现的tryAcquireShared
方法如下:
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors()) // 如果发现有线程在等待获取许可,就选择谦让
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
// 如果获取许可数大于当前已有的许可数,获取失败,且返回值小于0
// 如果CAS失败,则重新循环直到获取成功
// 如果CAS成功,说明获取成功,返回剩余的许可数
return remaining;
}
}
总体上是一个for
循环,这是为了应对CAS失败的情况。首先判断是否有线程在等待获取许可,如果有就选择谦让,这里体现了公平性
接下来判断获取之后剩余的许可数是否合法,如果小于0,说明没有足够的许可,获取失败,返回值小于0;如果大于0且CAS修改state
成功,说明获取许可成功,返回剩余的许可数
这里需要说明一下
tryAcquireShared
的返回值含义,这个其实在《全网最详细的ReentrantReadWriteLock源码剖析(万字长文)》也有讲解过:
- 负数:获取失败,线程会进入同步队列阻塞等待
- 0:获取成功,但是后续以共享模式获取的线程都不可能获取成功
- 正数:获取成功,且后续以共享模式获取的线程也可能获取成功
非公平模式
NonfairSync
类实现的tryAcquireShared
方法如下:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
实际上委托给了父类Sync
的nonfairTryAcquireShared
方法来执行:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
该方法是Sync
类中的一个final
方法,禁止子类重写。逻辑上和公平模式的tryAcquireShared
基本一致,只是没有调用hasQueuedPredecessors
,即使有其他线程在等待获取许可,也不会谦让,而是直接CAS竞争。这里体现了非公平性
acquire()
这个不带参数的acquire
方法,默认获取一个许可:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 默认获取一个许可
}
和acquire(int)
基本上是一样的,只是这里获取数固定为1。在公平模式和非公平模式下的获取方式也不相同。这里不再解释
忽略中断的获取
acquireUninterruptibly
是忽略中断的获取许可方法,也有两个重载版本。如果获取成功,则返回;如果许可不够而导致获取失败,则会进入AQS的同步队列阻塞等待。在整个过程中如果被中断,不会抛出中断异常,只会记录中断状态
acquireUninterruptibly(int)
首先来看看可以获取指定数量许可的acquireUninterruptibly
方法:
public void acquireUninterruptibly(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireShared(permits);
}
该方法会先检查permits
是否合法(非负数),再将后续的执行过程委托给Sync
类的父类AQS的acquireShared
方法来执行,其源码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
这里同样会调用子类实现的tryAcquireShared
方法,对于公平模式和非公平模式下的获取方式略有不同,在上面都已经分析过,这里不重复解释了
如果tryAcquireShared
获取成功,则直接返回;如果获取失败,则调用doAcquireShared
方法,进入同步队列阻塞等待,在等待过程中忽略中断,只记录中断状态<
和响应中断的acquire
方法相比,唯一的区别就在于如果获取失败,acquire
调用的是doAcquireSharedInterruptibly
,响应中断;而则这里的acquireUninterruptibly
调用的是doAcquireShared
,忽略中断
acquireUninterruptibly()
这个不带参数的acquireUninterruptibly
方法,默认获取一个许可:
public void acquireUninterruptibly() {
sync.acquireShared(1); // 默认获取一个许可
}
和acquireUninterruptibly(int)
基本上是一样的,只是这里获取数固定为1。在公平模式和非公平模式下的获取方式也不相同。这里也不再解释
获取剩余所有许可
这个是Semaphore
中比较特殊的一个获取资源的方式,它提供了drainPermits
方法,可以直接获取当前剩余的所有许可(资源),并返回获得的个数。其源码如下:
public int drainPermits() {
return sync.drainPermits();
}
该方法实际上委托给了Sync
类的drainPermits
方法来执行:
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
该方法是一个final
方法,禁止子类重写。整体上是一个for
循环,为了应对CAS失败的情况
首先通过AQS的getState
方法获取当前可用的许可数,再一次性全部获取,并返回获得的许可数,很简单~
释放许可
Semaphore
提供了release
作为释放许可的方法,和获取许可一样,release
也有两个重载版本。但是,释放许可和获取有两点不同:
- 释放许可的方法都是忽略异常的,没有响应异常的版本
- 对于公平模式和非公平模式来说,释放许可的方式都是一样的,因此在Sync类这一层就提供了统一的实现
release(int)
首先来看看释放指定数量许可的release
方法:
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
该方法会先检查permits
是否合法(非负数),再将后续的执行过程委托给Sync
类的父类AQS的releaseShared
方法来执行,其源码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
releaseShared
会先调用子类重写的tryReleaseShared
方法,在公平模式和非公平模式下的释放许可逻辑是一致的,因此在Sync
类就对其进行了统一的实现,而没有下放到子类中实现。Sync
类的tryReleaseShared
方法如下:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
这也是一个final
方法,禁止子类重写。整体上是一个for
循环,为了应对CAS失败的情况
循环体内对AQS的state
进行修改,不过需要避免释放许可后导致state
溢出,否则会抛出错误
使用Semaphore
的一点注意事项
从relsease(int)
的逻辑中,并没有发现对释放的数量有所检查,即一个线程可以释放任意数量的许可,而不管它真正拥有多少许可
比如:一个线程可以释放100个许可,即使它没有获得任何许可,这样必然会导致程序错误
因此,使用Semaphore
必须遵守“释放许可的数量一定不能超过当前持有许可的数量”这一规定,即使Semaphore
不会对其进行检查!
源码中的注释也指出了这一点:
There is no requirement that a thread that releases a permit must have acquired that permit by calling acquire. Correct usage of a semaphore is established by programming convention in the application.
release()
这个版本的release
默认释放一个许可:
public void release() {
sync.releaseShared(1);
}
其他和release(int)
一致,这里不再解释,不过使用release()
也必须注意遵守上面的规定,Semaphore
也不会主动进行检查
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
尝试获取许可
Semaphore
虽然提供了阻塞版本的获取方式,也提供了四个版本的尝试获取方式,包括两种:一种是非计时等待版本,一种是计时等待版本
非计时等待
tryAcquire(int)
public boolean tryAcquire(int permits) {
if (permits < 0) throw new IllegalArgumentException();
return sync.nonfairTryAcquireShared(permits) >= 0;
}
该方法会先检查permits
是否合法(非负数),再将后续的执行过程委托给Sync
类的nonfairTryAcquireShared
方法来执行。此方法就是非公平版本的尝试获取许可方式,即使当前Semaphore
使用的是公平策略。如果返回值不小于0,说明获取成功,返回true;否则获取失败,返回false。不管成功与否,都会立即返回,不会阻塞等待
tryAcquire()
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
这个就是默认获取一个许可的tryAcquire
版本,跟上面基本一样,不解释
计时等待
tryAcquire(int, long, TimeUnit)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
该方法会先检查permits
是否合法(非负数),再将后续的执行过程委托给Sync
类的父类AQS的tryAcquireSharedNanos
方法来执行。该方法会尝试获取许可,如果获取成功,则立即返回;如果获取不到,则阻塞一段时间。如果等待过程中被中断,则会抛出中断异常
tryAcquire(long, TimeUnit)
public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
这个就是默认获取一个许可的计时等待tryAcquire
版本,跟上面基本一样,不解释
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
其他方法
Semaphore
还提供了一些方法以获取信号量的状态,比如:
- 当前信号量使用的公平策略
- 当前可获取的许可数量
- 是否有线程正在等待获取许可
- 因为获取许可而被阻塞的线程数
下面一一来看
isFair
public boolean isFair() {
return sync instanceof FairSync;
}
如果Semaphore
的sync
域是FariSync
类对象,则说明使用的是公平策略,返回true,否则返回false
availablePermits
public int availablePermits() {
return sync.getPermits();
}
final int getPermits() {
return getState();
}
本质上调用的就是AQS的getState
方法,返回state
的值,而state
就代表了当前可获取的许可数量
hasQueuedThreads
public final boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
// AQS.hasQueuedThreads
public final boolean hasQueuedThreads() {
return head != tail;
}
本质上调用的是AQS的hasQueuedThreads
方法,判断同步队列的head
和tail
是否相同,如果相同,则说明队列为空,没有线程正在等待;否则说明有线程正在等待
getQueueLength
public final int getQueueLength() {
return sync.getQueueLength();
}
// AQS.getQueueLength
public final int getQueueLength() {
int n = 0;
for (Node p = tail; p != null; p = p.prev) {
if (p.thread != null)
++n;
}
return n;
}
这个方法实际调用的是AQS.getQueueLength
方法。此方法会对同步队列进行一个反向全遍历,返回当前队列长度的估计值。该值并不是一个确定值,因为在执行该函数时,其中的线程数可能已经发生了变化
Semaphore信号量源码解析的更多相关文章
- Java并发编程笔记之Semaphore信号量源码分析
JUC 中 Semaphore 的使用与原理分析,Semaphore 也是 Java 中的一个同步器,与 CountDownLatch 和 CycleBarrier 不同在于它内部的计数器是递增的,那 ...
- go中semaphore(信号量)源码解读
运行时信号量机制 semaphore 前言 作用是什么 几个主要的方法 如何实现 sudog 缓存 acquireSudog releaseSudog semaphore poll_runtime_S ...
- Java并发之Semaphore源码解析(二)
在上一章,我们学习了信号量(Semaphore)是如何请求许可证的,下面我们来看看要如何归还许可证. 可以看到当我们要归还许可证时,不论是调用release()或是release(int permit ...
- Java并发之ReentrantReadWriteLock源码解析(二)
先前,笔者和大家一起了解了ReentrantReadWriteLock的写锁实现,其实写锁本身实现的逻辑很少,基本上还是复用AQS内部的等待队列思想.下面,我们来看看ReentrantReadWrit ...
- 死磕 java同步系列之Semaphore源码解析
问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaph ...
- Java并发之Semaphore源码解析(一)
Semaphore 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析,ReentrantLock源码解析里介绍的方法有很多是本章的铺垫.下面,我们进入本章正题Sem ...
- 深入浅出Semaphore源码解析
Semaphore通过permits的值来限制线程访问临界资源的总数,属于有限制次数的共享锁,不支持重入. 前提条件 在理解Semaphore时需要具备一些基本的知识: 理解AQS的实现原理 之前有写 ...
- Flume-ng源码解析之Channel组件
如果还没看过Flume-ng源码解析之启动流程,可以点击Flume-ng源码解析之启动流程 查看 1 接口介绍 组件的分析顺序是按照上一篇中启动顺序来分析的,首先是Channel,然后是Sink,最后 ...
- ReactiveSwift源码解析(一) Event与Observer代码实现
ReactiveCocoa这个框架是做什么用的本篇博客就不做过多赘述了,什么是"响应式编程"也不多聊了,自行Google吧.本篇博客的主题是解析ReactiveCocoa框架中的核 ...
随机推荐
- Educational Codeforces Round 94 题解
我竟然比到了全场的 rk 14,incredible! A 大水题,直接输出 \(n\) 遍 \(s_n\) 即可. B 分类讨论题,放在 B 题可能难度有点大了. 直接暴力枚举你拿了多少个宝剑,然后 ...
- DirectX12 3D 游戏开发与实战第六章内容
利用Direct3D绘制几何体 学习目标 探索用于定义.存储和绘制几何体数据的Direct接口和方法 学习编写简单的顶点着色器和像素着色器 了解如何用渲染流水线状态对象来配置渲染流水线 理解怎样创建常 ...
- raid0 raid1 raid5
关于Raid0,Raid1,Raid5,Raid10的总结 RAID0 定义: RAID 0又称为Stripe或Striping,它代表了所有RAID级别中最高的存储性能.RAID 0提高存储性能 ...
- 用原生CSS编写-怦怦跳的心
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- 【leetcode】122.Best Time to Buy and Sell Stock II(股票问题)
You are given an integer array prices where prices[i] is the price of a given stock on the ith day. ...
- C语言把数字转换为字符串的函数
博主原文 C语言itoa()函数和atoi()函数详解(整数转字符C实现) C语言提供了几个标准库函数,可以将任意类型(整型.长整型.浮点型等)的数字转换为字符串. 1.int/float to st ...
- C++类的定义,成员函数的定义,对象的创建与使用
类是一个模板,可用类生成一系列可用的实例.例如 int B就是生成了一个符合int的数据B,类也是一样,使用类名就可以直接生成一个实例, 该实例中包含类中所有的数据类型和对这些数据的操作方法. 首先, ...
- VIM多标签页
:tabnew 增加一个标签 :tabc 关闭当前的tab :tabo 关闭所有其他的tab :tabp 或gT 前一个 :tabn 或gt 后一个 :tabs 显示 ...
- zabbix之修改中文
#在zabbix服务器安装中文名包 root@ubuntu:~# sudo apt-get install language-pack-zh* #:修改环境变量 root@ubuntu:~# sudo ...
- 连接 MySQL 数据库出现问题:The server time zone value ‘�й���ʱ��‘ is unrecogni....
出现问题 The server time zone value '�й���ʱ��' is unrecogni.... 解决方案 在 URL 后面加上 ?serverTimezone=UTC 如下: ...