JUC框架 系列文章目录
以获取独占锁的方法acquireQueued
为例
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 执行到这里,说明
// 已经尝试过获取锁了,但还是失败了(当然有可能是因为p != head)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
首先要知道调用acquireQueued
的线程一定是node
的代表线程,也就是最开始调用lock()
方法的那个线程,在acquireQueued
的死循环中,尽管可能 重复着 阻塞和被唤醒 的过程,但不管怎么说,执行acquireQueued
的任何代码的线程,一定是参数node
的代表线程。
与acquireQueued
相对,再以获取共享锁的doAcquireShared
方法为例:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//注意,这个node在这里
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 执行到这里,说明
// 已经尝试过获取锁了,但还是失败了(当然有可能是因为p != head)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
同样的,执行doAcquireShared
的任何代码的线程,一定是参数node
的代表线程。其次你也会发现,doAcquireShared
的方法逻辑,和acquireQueued
方法的逻辑大体一致,只是在获取锁成功后干的事情略有不同:
tryAcquire
返回了true,但调用的是setHead
。tryAcquireShared
返回了true,但调用的是setHeadAndPropagate
。doAcquireShared
里,而独占锁补上中断状态的时机是,acquireQueued
函数返回后,在acquire
里做的。总结:
shouldParkAfterFailedAcquire
的线程,一定是参数node
的代表线程,不管是独占锁还是共享锁。shouldParkAfterFailedAcquire
前,线程在此次循环中,已经尝试过获取锁了,但还是失败了。还是以独占锁和共享锁二者一起考虑。
parkAndCheckInterrupt
的线程,此时这个阻塞线程一定是head的后继。parkAndCheckInterrupt
的线程被中断了,此时线程马上被唤醒,继续for循环。任何时候别的线程都可以来中断阻塞线程。parkAndCheckInterrupt
的线程超时了,此时线程马上被唤醒,继续for循环。总结:
parkAndCheckInterrupt
的线程可以在任何时候被唤醒。首先分析独占锁:
public final boolean release(int arg) {
if (tryRelease(arg)) {//这里已经释放了独占锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
可见唤醒head后继的条件是,head的状态不为0就可以了。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
根据unparkSuccessor
的逻辑可知,在独占锁中,head的状态为0(因为if (ws < 0) compareAndSetWaitStatus(node, ws, 0);
),代表head的后继节点即将被唤醒,或者已经被唤醒。head的状态为0属于一种中间状态,因为:
SIGNAL
。shouldParkAfterFailedAcquire
,再把head的状态置为SIGNAL
。再看共享锁:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//这里已经释放了共享锁
doReleaseShared();
return true;
}
return false;
}
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; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
可见唤醒head后继的条件稍微有点苛刻,必须head的状态为SIGNAL
且设置状态从SIGNAL
变成0成功,才可以去唤醒head后继。
总之:
本函数,简单的讲就是把node的有效前驱(有效是指node不是CANCELLED的)找到,并且将有效前驱的状态设置为SIGNAL
,之后便返回true代表马上可以阻塞了。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 前驱节点已经设置了SIGNAL,闹钟已经设好,现在我可以安心睡觉(阻塞)了。
* 如果前驱变成了head,并且head的代表线程exclusiveOwnerThread释放了锁,
* 就会来根据这个SIGNAL来唤醒自己
*/
return true;
if (ws > 0) {
/*
* 发现传入的前驱的状态大于0,即CANCELLED。说明前驱节点已经因为超时或响应了中断,
* 而取消了自己。所以需要跨越掉这些CANCELLED节点,直到找到一个<=0的节点
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 进入这个分支,ws只能是0或PROPAGATE。
* CAS设置ws为SIGNAL
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
SIGNAL
,说明闹钟标志已设好,返回true表示设置完毕。CANCELLED
,说明前驱节点本身不再等待了,需要跨越这些节点,然后找到一个有效节点,再把node和这个有效节点的前驱后继连接好。SIGNAL
。由于shouldParkAfterFailedAcquire函数在acquireQueued
的调用中处于一个死循环中(独占锁对应的是acquireQueued
里的死循环,共享锁对应的是doAcquireShared
的死循环),且因为shouldParkAfterFailedAcquire函数若返回false,那么此函数必将至少执行两次才能阻塞自己。
shouldParkAfterFailedAcquire
只有在检测到前驱的状态为SIGNAL
才能返回true,只有true才会执行到parkAndCheckInterrupt
。shouldParkAfterFailedAcquire
返回false后,进入下一次循环,当前线程又会再次尝试获取锁(p == head && tryAcquire(arg)
)。或者说,每次执行shouldParkAfterFailedAcquire
,都说明当前循环 尝试过获取锁了,但失败了。compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
返回false并进入下一次循环,第二次才能进入if (ws == Node.SIGNAL)
分支,所以说 至少执行两次。SIGNAL
成功的。(考虑当前线程一直不能获取到锁) private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 为什么检测到0或PROPAGATE后,一定要设置成SIGNAL,
// 然后继续下一次循环(因为返回的false)。直接返回true不行吗
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
首先得思考,什么时候node
的前驱pred
的状态为0。
这种情况是最常见的,比如现在AQS的等待队列中有很多node正在等待,当前线程现在刚执行完毕addWaiter
(node刚成为新队尾),然后现在开始执行获取锁的死循环(独占锁对应的是acquireQueued
里的死循环,共享锁对应的是doAcquireShared
的死循环),此时node的前驱,也就是旧队尾的状态肯定还是0(也就是默认初始化的值),然后死循环执行两次,第一次执行shouldParkAfterFailedAcquire
自然会检测到前驱状态为0,然后将0设置为SIGNAL
;第二次执行shouldParkAfterFailedAcquire
,直接返回true。
很明显,这种情况线程会进行if (p == head && tryAcquire(arg))
这样的判断两次(共享锁也是这样,只不过是分开写的)。
if (p == head && tryAcquire(arg))
判断两次,是有好处的。node
的前驱(node.predecessor()
)if (p == head && tryAcquire(arg))
判断是很有必要的。pred
作为一个肯定不为队尾的节点,它的状态为0的情况,只能是本文章节释放锁后,唤醒head后继的条件中,所说的中间状态。这种中间状态只可能在head上出现,所以pred
肯定是head。所以node
现在是head后继。再结合前面总结出的两个结论:
parkAndCheckInterrupt
的线程可以在任何时候被唤醒。shouldParkAfterFailedAcquire
前,线程在此次循环中,已经尝试过获取锁了,但还是失败了。那么shouldParkAfterFailedAcquire中检测到0的情况可以是下面:
考虑本章场景,在shouldParkAfterFailedAcquire
中,如果检测到node
的前驱的状态为0,那么设置状态为SIGNAL,但设置状态其实是无所谓的。重点在于,这样shouldParkAfterFailedAcquire
会返回false,继续下一次循环,也就能再一次尝试获取锁。
按照本章场景,再一次尝试获取锁,肯定能成功。
检测到0后,一定要设置成SIGNAL的原因:
设置成SIGNAL后会返回false的原因:
PROPAGATE只能在使用共享锁的时候出现,并且只可能设置在head上。
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;
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//设置在head上
continue;
}
if (h == head)
break;
}
}
其实本章场景和上一章类似,当获取锁的下一次循环开始后,会发现再一次获取锁,就能成功了。
总结原因和上一章一样,这里就不写了。