信号量,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源代码之我见的更多相关文章

  1. C# 多线程之一:信号量Semaphore

    通过使用一个计数器对共享资源进行访问控制,Semaphore构造器需要提供初始化的计数器(信号量)大小以及最大的计数器大小 访问共享资源时,程序首先申请一个向Semaphore申请一个许可证,Sema ...

  2. 经典线程同步 信号量Semaphore

    阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <秒杀多线程第五篇经典线程同步关键段CS> <秒杀多线程第六篇经典线程同步事件Event& ...

  3. 互斥锁Mutex与信号量Semaphore的区别

    转自互斥锁Mutex与信号量Semaphore的区别 多线程编程中,常常会遇到这两个概念:Mutex和Semaphore,两者之间区别如下: 有人做过如下类比: Mutex是一把钥匙,一个人拿了就可进 ...

  4. 信号量 Semaphore

    一.简介         信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用,负责协调各个线程, 以保证它们能够正确.合理的使用公共资源. Semaphore可以控制某个资源可被同时 ...

  5. windows核心编程-信号量(semaphore)

    线程同步的方式主要有:临界区.互斥区.事件.信号量四种方式. 前边讲过了互斥器线程同步-----windows核心编程-互斥器(Mutexes),这章我来介绍一下信号量(semaphore)线程同步. ...

  6. 秒杀多线程第八篇 经典线程同步 信号量Semaphore

    阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <且不超过最大资源数量. 第三个參数能够用来传出先前的资源计数,设为NULL表示不须要传出. 注意:当 ...

  7. 转:【Java并发编程】之二十三:并发新特性—信号量Semaphore(含代码)

    载请注明出处:http://blog.csdn.net/ns_code/article/details/17524153 在操作系统中,信号量是个很重要的概念,它在控制进程间的协作方面有着非常重要的作 ...

  8. 多线程面试题系列(8):经典线程同步 信号量Semaphore

    前面介绍了关键段CS.事件Event.互斥量Mutex在经典线程同步问题中的使用.本篇介绍用信号量Semaphore来解决这个问题. 首先也来看看如何使用信号量,信号量Semaphore常用有三个函数 ...

  9. java笔记--对信号量Semaphore的理解与运用

    java Semaphore 信号量的使用: 在java中,提供了信号量Semaphore的支持. Semaphore类是一个计数信号量,必须由获取它的线程释放, 通常用于限制可以访问某些资源(物理或 ...

随机推荐

  1. 【Windows 操作系统】 内核对象|句柄

    内核对象简介 内核对象就是 一些数据结构该结构用来描述存储内核中的一个内存块中的数据信息.   内存块是一种数据结构,其中的数据成员负责维护该对象的相应信息,这个数据结构以及其中的数据成员只能由内核访 ...

  2. Java课程设计---创建数据库工具类

    1.传统的数据库操作 package com.java.mysql; import java.sql.Connection; import java.sql.DriverManager; import ...

  3. deepin下安装ESP32的开发环境

    deepin下安装ESP32的开发环境 本文章用于记录在deepin下安装ESP32的开发环境,前几日在CentOS7之下实现过,但CentOS7对于我这个Linux新手太不友好,于是重装deepin ...

  4. think php 路由增删改查(搜索+关键字标红+缩略图)

    路由 use think\Route; //展示添加表单 Route::get('create','user/user/create'); //表单提交数据 Route::post('save','u ...

  5. laravel 7 H-ui模板ajax批删

    1,HTML页面写一个button按钮 <a href="javascript:;" onclick="deleteAll()" class=" ...

  6. PHP 开发者如何做好密码保护 & Laravel 底层密码存储和验证实现

    随着在线攻击的增多,密码安全越来越重要.作为开发者我们要担负起安全管理.计算哈希和存储用户密码的责任,不管应用是简单的游戏还是绝密商业文件的仓库,都要做到这一点.PHP内置了一些工具,让保护密码变得更 ...

  7. Spring Bean生命周期,好像人的一生。。

    大家好,我是老三,上节我们手撸了一个简单的IOC容器五分钟,手撸一个Spring容器!,这节我们来看一看Spring中Bean的生命周期,我发现,和人的一生真的很像. 简单说说IoC和Bean IoC ...

  8. 快速整明白Redis中的整数集合到底是个啥

    整数集合简介 整数集合(intset)是Redis集合数据类型的内部编码之一,当集合数据类型中的元素都是整数并且元素数量较少的时候,Redis就使用整数集合作为内部编码. 整数集合(intset)中可 ...

  9. sql面试 case /union all

    1.sum(case when results='胜' then 1 else 0 end) as '胜' 要求查询出结果: sql语句实现: select date, sum(case when r ...

  10. vue+echarts可视化大屏,全国地图下钻,页面自适应

    之前写过一篇关于数据大屏及地图下钻的文章 https://www.cnblogs.com/weijiutao/p/13977011.html ,但是存在诸多问题,如地图边界线及行政区划老旧,无法自适应 ...