信号量,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