信号量,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. linux 测试机器端口连通性方法

    转至:https://blog.csdn.net/z1134145881/article/details/54706711 几种常用方法 下面一一介绍: 1 telnet方法 2 wget方法 3 s ...

  2. Go基础知识梳理(一)

    Go基础知识梳理(一) Go中package的用法及作用 package hello 用于分包,Go通过包来管理命名空间 import ( "hello" //通过import关键 ...

  3. WPF 布局之综合实例

    WPF 布局之综合实例 <Window x:Class="UniFormGridDemo.MainWindow" xmlns="http://schemas.mic ...

  4. emu8086 调用LED面板的方法

    一.实验要求 1.熟悉并掌握 EMU8086 汇编语言调试环境: 2.学习 8086 的指令系统,输入简单的指令,观察各寄存器.内存相关单元以及处理器标志位的变化(所有数据传送类指令,可参考教材用例) ...

  5. ActiveMQ-5.9-笔记-02

  6. Python可变参数*args和**kwargs

    本文我们将通过示例了解 Python函数的可变参数*args和 **kwargs的用法. 知识预备:Python 函数和 Python 函数参数 在Python编程中,我们定义一个函数来生成执行类似操 ...

  7. Docker——网络

    docker0 查看主机的ip [root@iZwz908j8pbqd86doyrez5Z test]# ip addr #本机回环地址 1: lo: <LOOPBACK,UP,LOWER_UP ...

  8. web服务器-nginx默认网站

    web服务器-nginx默认网站 一 默认网站 server { listen 80; server_name localhost; location / { root html; index ind ...

  9. Apache+PHP+Mysql安装手册(Windows)

    一,准备安装包 下载地址: Apache:HTTPS://www.apachelounge.com/download/ PHP:http://php.net/downloads.php MySQL h ...

  10. Apache+PHP+Mysql安装手册(Linux)

    一. 检查系统环境 1.确认centos版本 [root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Co ...