java acquire()_Java AbstractQueuedSynchronizer源码阅读3-cancelAcquire()

cancelAcquire()的作用

Cancels an ongoing attempt to acquire。

cancelAcquire()的使用场景

调用了cancelAcquire()的接口如下所示。

01f2046aab64

调用了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出队的过程如下图所示。

01f2046aab64

出队操作1

结合代码:

cancelAcquire()调用compareAndSetTail()方法将tail指向pred

cancelAcquire()调用compareAndSetNext()方法将pred的next指向空

场景2. node既不是tail,也不是head的后继节点

node出队过程如下图所示。

01f2046aab64

出队操作2

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*表示前继节点)

01f2046aab64

出队操作3

指针的变动和场景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节点关联的线程置空(在图中,使用白色背景色的方框表示未关联到任何线程的节点)。

01f2046aab64

head的更新

对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指针是不可靠的有关,也或许有性能方面的考虑,并不理解呀。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值