cancelAcquire()的作用
Cancels an ongoing attempt to acquire。
cancelAcquire()的使用场景
调用了cancelAcquire()的接口如下所示。
这些接口的代码的代码结构类似,均是采取for(;;)循环的形式,不停的尝试获取锁。一旦发生异常,导致获取锁失败,则会调用cancelAcquire()方法"Cancels an ongoing attempt to acquire"。它们的代码结构均如下所示:
boolean failed = true;
try {
for (;;) {
...
}
} finally {
if (failed)
cancelAcquire(node);
}
cancelAcquire()的操作
cancelAcquire()的主要操作有两类:
清理状态
- node不再关联到任何线程
- node的waitStatus置为CANCELLED
node出队
包括三个场景下的出队:
- node是tail
- node既不是tail,也不是head的后继节点
- node是head的后继节点
这里的分类是不是有些奇怪。
为何不是如下的分类呢?
- node是tail
- node是head
- node既不是tail,又不是head
这样一头一尾和中间,不才是一个规整完美的分类么?后续再说。
cancelAcquire()的出队详解
下面结合cancelAcquire()的代码对出队操作进行详述。
cancelAcquire()如下,其中有对应的注释。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
//1. node不再关联到任何线程
node.thread = null;
//2. 跳过被cancel的前继node,找到一个有效的前继节点pred
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
//3. 将node的waitStatus置为CANCELLED
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
//4. 如果node是tail,更新tail为pred,并使pred.next指向null
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
//
int ws;
//5. 如果node既不是tail,又不是head的后继节点
//则将node的前继节点的waitStatus置为SIGNAL
//并使node的前继节点指向node的后继节点
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//6. 如果node是head的后继节点,则直接唤醒node的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
代码注释中的4、5、6三步即对应到node出队的三个场景。
下面,结合代码,对每个场景的出队进行详述。要注意到的是,AbstractQueuedSynchronizer维护的是一个双向队列,每个 node都有一个prev指针和next指针。
场景1. node是tail
node出队的过程如下图所示。
结合代码:
cancelAcquire()调用compareAndSetTail()方法将tail指向pred
cancelAcquire()调用compareAndSetNext()方法将pred的next指向空
场景2. node既不是tail,也不是head的后继节点
node出队过程如下图所示。
cancelAcquire()调用了compareAndSetNext()方法将pred指向successor。虽然代码里这一部分有一堆判断,但是实际上起到出队作用的就这句。
不过,还少了一步呀。将successor指向pred是谁干的?
是别的线程做的。当别的线程在调用cancelAcquire()或者shouldParkAfterFailedAcquire()时,会根据prev指针跳过被cancel掉的前继节点,同时,会调整其遍历过的prev指针。代码类似这样;
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
场景3.node是head的后继节点
node出队的过程如下图所示(图中用node*表示前继节点)
指针的变动和场景2如出一辙。
结合代码:
cancelAcquire()调用了unparkSuccessor()
不过,unparkSuccessor()中并没有对队列做任何调整呀。
比场景2还糟糕,这次,cancelAcquire()对于出队这件事情可以说是啥都没干。
出队操作实际上是由unparkSuccessor()唤醒的线程执行的。
unparkSuccessor()会唤醒successor关联的线程(暂称为sthread),当sthread被调度并恢复执行后,将会实际执行出队操作。
现在需要搞清楚sthread是从什么地方恢复执行的呢?这要看sthread是在哪里被挂起的。在哪里跌倒的,就在哪里站起来。
本文开头在使用场景中,列出了调用cancelAcquire()的所有接口,也正是在这些接口中,线程将有可能被挂起。这些方法的代码结构类似,主体是一个for循环。这里以acquireQueued()为例,如下所示:
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()//当初就是被这个方法挂起的)
interrupted = true;
}
sthread当初就是被parkAndCheckInterrupt()给挂起的,恢复执行时,也从此处开始重新执行。sthread将会重新执行for循环,此时,node尚未出队,successor的前继节点依然是node,而不是head。所以,sthread会执行到shouldParkAfterFailedAcquire()处。而从场景2中可以得知,shouldParkAfterFailedAcquire()中将会调整successor的prev指针(同时也调整head的next指针),从而完成了node的出队操作。
接下来还有一些补充的说明
场景3中,node出队后,head的设置
接续上面的步骤,当node的successor关联的线程被唤醒后,会重新执行for循环。此时,因successor的前继仍是node,而非head,所以会执行shouldParkAfterFailedAcquire()。successor会跳过被cancel的node,从而成为head的后继节点。下次再次调用for循环时,successor的前继已经更新为head,就会进入上述for循环中的第一个if,更新队列的head。head的更新过程如下所示,head会更新为successor节点,并将successor节点关联的线程置空(在图中,使用白色背景色的方框表示未关联到任何线程的节点)。
对head的理解
从setHead()的实现以及所有调用的地方可以看出,head指向的节点必定是拿到锁(或是竞争资源)的节点,而head的后继节点则是有资格争夺锁的节点,再后续的节点,就是阻塞着的了。
head指向的节点,曾经关联的线程必定已经获取到资源,在执行了,所以head无需再关联到该线程了。head所指向的节点,也无需再参与任何的竞争操作了。
现在再来看node出队时的分类,就好理解了。head既然不会参与任何资源竞争了,自然也就和cancelAquire()无关了。
场景3中,unparkSuccessor是必须的么?可以模仿场景2的做法么?
场景3中的做法大约是:被cancel的node是head的后继节点,是队列中唯一一个有资格去尝试获取资源的节点。他将资格放弃了,自然有义务去唤醒他的后继来接棒。
感觉按照场景2中的做法,逻辑上似乎也是完备的?不过此时,successor需要等待正在占用资源的线程主动释放资源才能被唤醒?
为何这样设计出队呢?
cancelAcquire()是一个出队操作,出队要调整队列的head、tail、next和prev指针。
对于next指针和tail,cancelAcquire()使用了一堆CAS方法,本着一种别人不上,我上,别人上过了,我不能再乱上了的态度。这是一种积极主动的做事方式。
而对于prev指针和head,cancelAcquire()则是完全交给别的线程来做,感觉像是lazy模式。
为何是这样的实现呢?为何不全采用lazy模式,或者是全采用积极主动的方式?
这似乎和prev指针是可靠的,而next指针是不可靠的有关,也或许有性能方面的考虑,并不理解呀。