信号量,semaphore源代码之我见
信号量,Semaphore,一个限定访问线程数量的工具类,属于并发包java.util.concurrent 里面的类。
Semaphore,内部提供了构造方法(包含默认的非公平信号量构造方法,已经可设置是否公平的构造方法)、获取信号量acquire()、尝试获取信号量tryAcquire()、
规定时间内尝试获取信号量tryAcquire(long timeout, TimeUnit unit)、
释放信号量release()、获取可用的信号量数量availablePermits()、重置信号drainPermits()、叠减信号量reducePermits(int reduction)等方法。其中,获取信号量、释放信
号量,均有多个重载,区别于是否抛出异常、是否有时间限制、获取及释放信号量的数量。
其中,最重要的是获取信号量acquire() 以及释放信号量 release()
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Semaphore和可重入锁ReenTrantLock很相似,都有一个内部类Sync。这个内部类Sync继承了AbstractQueuedSynchronizer(AQS),除了父类AQS的原有方法外,还
构造方法Sync(int permits)、获取信号量getPermits()、尝试获取信号量final int nonfairTryAcquireShared(int acquires)、
尝试释放信号量tryReleaseShared(int releases)、叠减信号量reducePermits(int reductions)、信号量归零(重置)drainPermits() 几个方法。
Sync类,有两个子类,非公平信号量NonfairSync 以及 公平信号量FairSync。两个子类,都有两个方法:构造方法、以及从超类里继承的抽象方法
int tryAcquireShared(int acquires)。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
先看构造方法Semaphore(int permits)。源代码如下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
从上面可以看出,信号量默认使用的是非公平信号量。这个是符合计算机的最大计算机资源使用率的思想的。
获取信号量acquire(),源代码如下:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//获取共享信号量,这个方法会抛出异常
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //如果线程被中断,则抛出线程中断异常
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) //尝试获取信号量,如果失败,表示信号量已经被获取完了。这个tryAcquireShared从AQS继承是由Sync的子类实现
doAcquireSharedInterruptibly(arg); } //以共享可中断模式获取信号量
//尝试获取公平信号量
protected int tryAcquireShared(int acquires) {
for (;;) { //死循环,表现在运行期间,就是阻塞。也就是说,所谓的运行期间阻塞,在代码里,本质上是执行了死循环。
if (hasQueuedPredecessors()) //因为公平信号量,始终是从第一个节点开始运行的。所以,如果没有前驱节点,直接返回-1,失败
return -1;
int available = getState(); //获取信号量的数量。从这里也可以看出,信号量的数量,是放在AQS的由volatile修饰state字段的。
int remaining = available - acquires;
if (remaining < 0 || //如果信号量 <0,直接返回信号量的剩余数量。否则,执行CAS操作,设置剩余信号量
compareAndSetState(available, remaining))
return remaining;
}
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
} final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
从上面可以看出,公平信号量和非公平信号量,代码大同小异。区别在于,公平信号信号量,总是从头节点开始的。
下面看 以共享可中断模式获取信号量doAcquireSharedInterruptibly(arg)
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED); //增加waiter,这个节点是共享模式的
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor(); //当前节点的前驱节点
if (p == head) { //如果当前节点的前驱节点是头节点(至于为什么是前驱节点而不是当前节点,原因参考enq方法)
int r = tryAcquireShared(arg); //再次尝试获取信号量
if (r >= 0) { //如果成功,则将当前的节点设置成头节点,并释放信号量
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && //检查是否应该挂起失败的线程。这里通常返回的false,不会抛出异常
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node); //注销当前节点
}
}
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
//将当前线程封装进node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//把节点添加进资源队列末尾tail
Node pred = tail;
if (pred != null) { //如果末尾节点不为空,则采用CAS操作将当前节点添加进队列末尾
node.prev = pred; //当前节点的前驱节点,设置为末尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); //初始化队列
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) { //采取死循环,添加节点
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) //如果末尾节点为空,则证明队列为空,新添加一个节点,作为头结点和末尾节点
tail = head;
} else { //已经有末尾节点,则采取CAS操作把当前节点添加进队列末尾,成为末尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//注意这里的代码实际执行效果。分为两步:1、先检查末尾节点是否为空,如果空,证明队列为空,则添加一个新节点,同时作为头节点和末尾界定啊
//2、如果末尾节点不为空(队列已经初始化),则采用CAS操作把当前节点添加队列末尾
//其中,步骤2是肯定会执行的 /**
* Sets head of queue, and checks if successor may be waiting
* in shared mode, if so propagating if either propagate > 0 or
* PROPAGATE status was set.
*
* @param node the node
* @param propagate the return value from a tryAcquireShared
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
//如果当前节点没有了下一个节点或者下一个节点已经是共享模式,则可释放当前节点。
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
/**
* Release action for shared mode -- signals successor and ensures
* propagation. (Note: For exclusive mode, release just amounts
* to calling unparkSuccessor of head if it needs signal.)
*/
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { //如果status已经是SIGNAL了,将当前节点的status使用CAS设置成0。注意这里,有continue
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); //不挂起后继节点
}
else if (ws == 0 && //status设置成0或者本身已经是0,则采取CAS操作,将status设置成PROPAGATE,也就是-3状态
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed //如果头节点没有变化,证明头节点没有被其他节点修改,则释放成功
break;
}
}
//从这里看出,所谓的释放信号,就是将头结点(刚才已经把获取到信号量的节点设置成头节点)的状态设置成PROPAGATE -3的状态
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev; //死循环直到找到状态<=0的前驱节点
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//如果前驱节点已经是SIGNAL -1 唤醒状态,直接返回true。
//但是对于信号量的实际执行,前驱节点不是SIGNAL,则执行到else代码块
//所以,这个方法的实际作用,就是讲前驱节点的状态设置成SIGNAL,并返回false
总结:获取信号量的时候,如果信号量还有,则获取成功。否则,将获取失败的线程,添加进队列里面(包含队列初始化)。
然后,采用死循环执行阻塞。当一个线程释放了信号量后,队列的第二个节点(注意这里不是头节点)会采用死循环获取信号量,成功后则将自设置成头节点,
同时设置自己的状态为PROPAGATE -3。这其中还包括线程中断后,挂起线程。挂起后驱节点、注销节点等其他辅助性操作。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
下面看下释放信号量release()
public void release() {
sync.releaseShared(1); //这个方法,调用父类AQS的releaseShared()方法
}
//释放共享信号量
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { //尝试释放信号量,这里调用的是Semaphore的方法
doReleaseShared(); //这里真正释放信号量,参考上面的代码说明
return true;
}
return false;
}
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;
}
}
从这里可以看出,所谓的尝试释放信号量,就是采用CAS操作,将AQS的state变量叠减。
而真正释放信号量,则是将头节点的状态改成PROPAGATE -3
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
其他方法
获取可用信号量数量,availablePermits(),就是获取AQS的state变量的值。
重置信号量,drainPermits(),采用CAS操作将state重置为0
叠减信号量,reducePermits(int reduction),采用CAS操作叠减
是否形成队列,boolean hasQueuedThreads(),判断头节点是否不等于末尾节点
另外:
根据源代码说法,如果同时有两个线程a和b,a线程执行release(),b线程执行acquire(),那么根据happen-before规则,a线程将会在b线程之前执行。
Memory consistency effects: Actions in a thread prior to calling
* a "release" method such as {@code release()}
* <a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
* actions following a successful "acquire" method such as {@code acquire()}
* in another thread
信号量,semaphore源代码之我见的更多相关文章
- C# 多线程之一:信号量Semaphore
通过使用一个计数器对共享资源进行访问控制,Semaphore构造器需要提供初始化的计数器(信号量)大小以及最大的计数器大小 访问共享资源时,程序首先申请一个向Semaphore申请一个许可证,Sema ...
- 经典线程同步 信号量Semaphore
阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <秒杀多线程第五篇经典线程同步关键段CS> <秒杀多线程第六篇经典线程同步事件Event& ...
- 互斥锁Mutex与信号量Semaphore的区别
转自互斥锁Mutex与信号量Semaphore的区别 多线程编程中,常常会遇到这两个概念:Mutex和Semaphore,两者之间区别如下: 有人做过如下类比: Mutex是一把钥匙,一个人拿了就可进 ...
- 信号量 Semaphore
一.简介 信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用,负责协调各个线程, 以保证它们能够正确.合理的使用公共资源. Semaphore可以控制某个资源可被同时 ...
- windows核心编程-信号量(semaphore)
线程同步的方式主要有:临界区.互斥区.事件.信号量四种方式. 前边讲过了互斥器线程同步-----windows核心编程-互斥器(Mutexes),这章我来介绍一下信号量(semaphore)线程同步. ...
- 秒杀多线程第八篇 经典线程同步 信号量Semaphore
阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <且不超过最大资源数量. 第三个參数能够用来传出先前的资源计数,设为NULL表示不须要传出. 注意:当 ...
- 转:【Java并发编程】之二十三:并发新特性—信号量Semaphore(含代码)
载请注明出处:http://blog.csdn.net/ns_code/article/details/17524153 在操作系统中,信号量是个很重要的概念,它在控制进程间的协作方面有着非常重要的作 ...
- 多线程面试题系列(8):经典线程同步 信号量Semaphore
前面介绍了关键段CS.事件Event.互斥量Mutex在经典线程同步问题中的使用.本篇介绍用信号量Semaphore来解决这个问题. 首先也来看看如何使用信号量,信号量Semaphore常用有三个函数 ...
- java笔记--对信号量Semaphore的理解与运用
java Semaphore 信号量的使用: 在java中,提供了信号量Semaphore的支持. Semaphore类是一个计数信号量,必须由获取它的线程释放, 通常用于限制可以访问某些资源(物理或 ...
随机推荐
- BI工具做数据可视化项目频频失败的原因
现如今数据可视化可谓是非常之火,随着硬件价格的一降再降,仿佛做数据可视化项目,你没有数据大屏,你就没有逼格.理想很丰满,现实很骨感,并不是每一个数据可视化项目都能够成功.数据可视化项目的进行,无外乎是 ...
- 【C# 编码格式】 System.Text 命名空间 Encoding
Encoding基类 System.Text.ASCIIEncoding类 System.Text.UnicodeEncoding类 System.Text.UTF32Encodin ...
- (二) operator、explicit与implicit 操作符重载
有的编程语言允许一个类型定义操作符应该如何操作类型的实例,比如string类型和int类型都重载了(==)和(+)等操作符,当编译器发现两个int类型的实例使用+操作符的时候,编译器会生成把两个整 ...
- 按照TomCat版本重新配置web.xml文件
在TomCat的目录:C:\Program Files\Apache Software Foundation\Tomcat 9.0_Tomcat9.2\webapps\ROOT\WEB-INF下的we ...
- Windows安装face_recognition库
写在前面: 在pip官网搜face_recognition https://pypi.org/project/face_recognition/ 介绍中可看出该包更适合在Linux系统中使用,但也附上 ...
- 『现学现忘』Docker基础 — 23、使用Docker安装Tomcat
目录 步骤1:搜索镜像 步骤2:下载Tomcat镜像 步骤3:运行Tomcat镜像 步骤4:本机和外网测试 步骤5:解决问题 补充:--rm选项 步骤1:搜索镜像 使用docker search命令进 ...
- IDEA快捷键使用分享
Ctrl+D:复制当前行 Shift+Enter:光标移动到下一行 Alt+/:补全代码 Alt+Enter:万能解错/生成返回值变量 Ctrl+Z:撤销 Ctrl+y:反撤销 Ctrl+y:删除 C ...
- PicGo+Typora配置gitee图床
1 .下载Typora Typora官网地址:https://www.typora.io/#windows 本文使用的Typora版本为 0.11.2 根据自己的要求进行安装即可! 2.下载PicGo ...
- Redis常见延迟问题定位与分析
Redis作为内存数据库,拥有非常高的性能,单个实例的QPS能够达到10W左右.但我们在使用Redis时,经常时不时会出现访问延迟很大的情况,如果你不知道Redis的内部实现原理,在排查问题时就会一头 ...
- [apue] linux 文件系统那些事儿
前言 说到 linux 的文件系统,好多人第一印象是 ext2/ext3/ext4 等具体的文件系统,本文不涉及这些,因为研究具体的文件系统难免会陷入细节,甚至拉大段的源码做分析,反而不能从宏观的角度 ...