到目前为止已经知道了AQS中同步队列的基本的工作原理可以总结为维护同步队列,获取资源和改变线程状态。上一篇文章中主要总结了独占模式下的资源获取AQS详解独占模式资源的获取与释放。这篇主要总结一下AQS中的共享模式。
共享模式从字面上理解就是,这个资源可以被多个线程共享。在独占模式下,我们知道state的状态最初的值是0.如果某个线程获取到资源了state就加了1释放资源了就减去1。当state变为0的时候唤醒后继节点的线程,让后继节点的线程去持有资源。那么好了我们可以不可以这么干,一开始我给state设定一个值,当一个线程获取资源后,我的state就减去1,其它在来时我在减去1...以此类推,直到线程获取资源减到为0为止。表示资源没有了其他线程就无法获取了只能去等待了。这样的话多个线程就将这个state共享了,其实这就是AQS中的共享模式。
上面我们已经总出了它的大体逻辑了,可能在实现中还有一些细节需要去理解。下面看共享资源的获取
/**
*共享资源的获取
*
* @param arg 要获取的资源数
*/
public final void acquireShared(int arg) {
//获取共享锁小于0表示资源没有了,也就获取失败了
if (tryAcquireShared(arg) < 0) {
//真正的去获取资源的方法
doAcquireShared(arg);
}
}
上面的方法中有关tryAcquireShared()我们就不必看了,看的话也是抛了一个异常,但是可以去看看它子类的实现比如Semaphore的实现,如果小于0表示没有获取到资源。看一下没有获取到资源会怎么做,下面是源码。
/**
* 共享模式的资源竞争
*
* @param arg 资源
*/
private void doAcquireShared(int arg) {
//添加一个共享节点到同步队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
//是否中断
boolean interrupted = false;
for (; ; ) {
//获取当节点的前驱节点
final Node p = node.predecessor();
if (p == head) {
//如果前驱节点时头接节点的话,再次获取资源r代表的是剩余资源
int r = tryAcquireShared(arg);
if (r >= 0) {
//大于0获取到了就要检查还有没有资源,有的话还要去继续去唤醒下一个节点
setHeadAndPropagate(node, r);
p.next = null;
if (interrupted) {
selfInterrupt();
}
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
上面的方法和独占模式下的整体上没有太大的区别,但是在共享模式下获取到资源后加了一个setHeadAndPropagate()这个方法,这个方法是用来判断资源还有剩余且下一个节点是否是共享节点的。下面是源码
/**
* 设置队列头部,并检查后继者是否在等待 在共享模式下,
*
* @param node node节点
* @param propagate 剩余资源数
*/
private void setHeadAndPropagate(Node node, int propagate) {
//获取头节点
Node h = head;
//将刚获取到资源的节点设置为头节点
setHead(node);
//propagate>0表示还有资源可以获取
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前节点的下一个节点为空,或者下一个节点是共享
//节点的时
if (s == null || s.isShared()) {
//释放资源
doReleaseShared();
}
}
}
上面这个方法用来将刚获取到资源的节点设置为头节点,同时还检查了有没有资源和有没有共享节点。如果有的话释放资源(注意的释放资源并没有操作state只是做了唤醒下一个节点,由下一个节点的线程去操作资源)下面是源码
/**
*
*
* 唤醒下一个节点
*/
private void doReleaseShared() {
for (; ; ) {
//获取头
Node h = head;
//如果头节点不为空,又不是尾节点的话
if (h != null && h != tail) {
//获取当前头节点的状态
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//更新成功了的话唤醒下一个节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
continue;
} else {
//唤醒下一个节点
unparkSuccessor(h);
}
//将节点状态设置为-3传播状态
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
continue;
}
}
//这里当唤醒线程的时候这里的头节点head是可能变得,一但获取成功就变了,而h还是以前头节点的引用
if (h == head) {
break;
}
}
}
这个方法可能有这样一个问题,这个循环不就循环一次吗?为什么要用死循环呢?其实不是的,h是一个头节点的引用,这个h可能是一个旧的头节点,因为在上一个方法中有设置头节点的方法,head是获取资源的线程节点,在成功的唤醒了下一个节点而且被唤醒的节点前驱节点又是头节点,资源有存在且没有中断的话是可以获取成功的获取资源的,那么这个head就有可能是被修改了,所以for循环才会一直继续.。才会继续去拿资源一直拿到没有为止。
在上面的方法中已经介绍了资源的释放,而且资源的释放就是操作state和唤醒下一个节点,下面是源码的
/**
* 释放资源
*
* @param arg 要释放的资源数
* @return 是否释放成功
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
调用tryReleaseShared(int)由子类实现去操作并state,返回boolean内部调用doReleaseShared()方法。doReleaseShared()方法在上文已经介绍过了。
共享模式的资源获取与释放主要在于将资源预先初始化一定数目,然后各个线程去获取特定数量的资源,在获取资源时如果资源还有剩下就唤醒后继节点继续获取,知道资源没有剩余,其他资源只能进入等待状态。释放时做了操作资源和唤醒下一个节点的操作。