本文是在AQS(AbstractQueuedSynchronizer)源代码分析(一)和AQS(AbstractQueuedSynchronizer)源代码分析(二)两篇文章的基础上编写的。关于shouldParkAfterFailedAcquire,addWaiter,parkAndCheckInterrupt,park,unpark,unparkSuccessor方法,如果不清楚,可以参考前两篇文章,本文不再做说明。
AQS中的非共享模式与共享模式的区别在于,非共享模式每次只能有一个线程执行被锁定的内容,即目标语句或者目标方法。比如ReentrantLock类,在lock方法锁定的范围内,只会同时有一个线程在执行。而共享模式则是可以多个线程同时执行被锁定内容的。典型的应用类就是Semaphore。在设定初始信号量的值之后,比如是5;那么就会有五个线程同时执行。
一、共享的acquire方法
public final void acquireShared(int arg) {
//这个方法由子类实现。这里使用Semaphore.NonfairSync类中的tryAcquireShared进行说明
if (tryAcquireShared(arg) < 0)
//返回值小于0,则表示sate值已经被使用完,即已经有5个线程在运行。其他线程不能再进入了
doAcquireShared(arg);
}
//类Semaphore.NonfairSync
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
/**
* 当使用Semaphore时会有如下的代码:
* Semaphore semaphore = new Semaphore(5);
* semaphore.acquire();
* ……
* semaphore.release();
*
* 这里的getSate,如果是第一次进行semaphore.acquire()的话,
* getSate返回5,即我们设定的初始值
* 每一次semaphore.acquire()都会对将getSate的返回值进行减少,
* 即:int remaining = available - acquires,这段代码的含义
*/
for (;;) {
int available = getState(); //获取sate状态
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining; //这里会返回本次acquire之后,还剩余的sate值
}
}
上面的代码演示了Semaphore是如何进行信号量的获取操作的。当信号量获取失败之后,就会执行doAcquireShared(arg)方法。看一下doAcquireShared(arg)方法的代码:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); //向队列增加节点,追加的node是共享模式的node
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //死循环
final Node p = node.predecessor(); //获取当前节点的prevNode
if (p == head) {
int r = tryAcquireShared(arg); //如果是prevNode节点如果是head节点,则再次尝试acquire
if (r >= 0) {
setHeadAndPropagate(node, r); //再次尝试,获取成功
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && //再次尝试,获取失败,挂起线程。可以参考前两篇文章
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这段代码的执行逻辑与非共享模式的执行逻辑相似,这里不赘述。我们重点关注的是“当再次尝试,获取成功之后的setHeadAndPropagate方法的处理”。这个方法的处理是与非共享模式不一样的。setHeadAndPropagate的代码:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//替换head节点。因为head表示当前正在执行的节点。
//节点已经获取成功,则将会被执行,所以需要替换head
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared()) //获取当前节点的下一个节点,判断是否是共享模式的节点
//如果是共享模式的节点,则唤醒nextNode。doReleaseShared方法其实就是semaphore.release()
//的内部实现。具体的实现,下文release方法中进行说明
doReleaseShared();
}
}
可以看到,在非共享模式中,如果获取成功后,仅仅只是直接替换了head节点,并没有唤醒nextNode节点的操作;而对于共享模式,则进行了唤醒操作。所以这个唤醒的操作将是共享模式的核心。
二、共享的release方法
代码:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { //尝试释放,如果释放成功,则执行doReleaseShared
doReleaseShared();
return true;
}
return false;
}
类Semaphore.Sync中的tryReleaseShared(arg)方法的实现如下:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases; //acquire是减少,release就是增加
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
接下来重点看一下doReleaseShared()方法:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus; //获取head节点的状态
//判断head节点的状态,如果head节点的状态是SIGNAL,
//表示head节点存在nextNode,需要唤醒nextNode
if (ws == Node.SIGNAL) {
//先尝试修改head节点的状态,如果成功,则唤醒nextNode,如果失败,则继续尝试
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
//如果head节点的状态是0,则尝试修改为PROPAGATE;
//PROPAGATE表示下一个节点可以无条件的进行acquire,也就是下一次acquire一定会成功,即信号量充足
//如果head节点的状态是PROPAGATE,则不做任何处理,直接退出循环返回。
}
if (h == head) //跳出循环
break;
}
}
以上基本就是AQS的共享模式的实现。对于doReleaseShared()方法中对于head节点的SIGNAL状态判断,很多人可能会有疑惑。疑惑在于:head节点什么时候才会为SIGNAL状态呢?
对于这个疑惑,首先先要知道SIGNAL状态表示什么。SIGNAL状态表示节点有nextNode,即有后续处理节点。在当前的节点处理完之后,需要唤醒nextNode。以Semaphore类举例:
当我们的5个信号量都用完的时候,有新的线程进入(下文称作newThread)。此时,newThread就会被挂起。这个挂起的过程需要经过两个个步骤:
1、在node队列增加一个node,且这个node是共享模式的
2、判断新增加的node的prevNode(前节点)是否为SIGNAL状态。如果是SIGNAL状态,则直接挂起线程;如果不是SGINAL状态,则先将前一个node的状态修改SIGNAL状态,再挂起线程。
关于上面两个步骤的实现在shouldParkAfterFailedAcquire方法和doAcquireShared方法中