Java AbstractQueuedSynchronizer源码阅读3-cancelAcquire()

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容