Java AbstractQueuedSynchronizer源码阅读3-cancelAcquire()

0.07字数 1720阅读 2378

cancelAcquire()的作用

Cancels an ongoing attempt to acquire。

cancelAcquire()的使用场景

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

调用了cancelAcquire()的所有接口

这些接口的代码的代码结构类似,均是采取for(;;)循环的形式,不停的尝试获取锁。一旦发生异常,导致获取锁失败,则会调用cancelAcquire()方法"Cancels an ongoing attempt to acquire"。它们的代码结构均如下所示:

boolean failed = true;
try {
    for (;;) {
        ...
    }
} finally {
    if (failed)
        cancelAcquire(node);
}

cancelAcquire()的操作

cancelAcquire()的主要操作有两类:

清理状态

  1. node不再关联到任何线程
  2. node的waitStatus置为CANCELLED

node出队

包括三个场景下的出队:

  1. node是tail
  2. node既不是tail,也不是head的后继节点
  3. node是head的后继节点

这里的分类是不是有些奇怪。
为何不是如下的分类呢?

  1. node是tail
  2. node是head
  3. 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出队的过程如下图所示。

出队操作1

结合代码:
cancelAcquire()调用compareAndSetTail()方法将tail指向pred
cancelAcquire()调用compareAndSetNext()方法将pred的next指向空

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

node出队过程如下图所示。

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

出队操作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节点关联的线程置空(在图中,使用白色背景色的方框表示未关联到任何线程的节点)。

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

推荐阅读更多精彩内容