天天看點

AQS喚醒線程的時候為什麼從後向前周遊

​先來熟悉一下代碼,挂起和喚醒這兩部分​

  1. 尾部周遊源碼
private void unparkSuccessor(Node node) {
    //擷取wait狀态
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);// 将等待狀态waitStatus設定為初始值0
    /**
     * 若後繼結點為空,或狀态為CANCEL(已失效),則從後尾部往前周遊找到最前的一個處于正常阻塞狀态的結點
     * 進行喚醒
     */
    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);//喚醒線程
}      

注意for循環中的邏輯:從尾部開始向前周遊,找到最前的一個處于正常阻塞狀态的結點,直到節點重合(即等于目前節點)

  1. 高并發下入隊邏輯

    既然采用了從尾部周遊的邏輯,那麼肯定是為了解決可能會出現的問題。而這個問題就在enq(…)方法中

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            //隊列為空需要初始化,建立空的頭節點
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            //set尾部節點
            if (compareAndSetTail(t, node)) {//目前節點置為尾部
                t.next = node; //前驅節點的next指針指向目前節點
                return t;
            }
        }
    }
}      
  1. 原子性問題

    在該段方法中,将目前節點置于尾部使用了CAS來保證線程安全,但是請注意:在if語句塊中的代碼并沒有使用任何手段來保證線程安全!

    也就是說,在高并發情況下,可能會出現這種情況:

    線程A通過CAS進入if語句塊之後,發生上下文切換,此時線程B同樣執行了該方法,并且執行完畢。然後線程C調用了unparkSuccessor方法。

    假如是從頭到尾的周遊形式,線程A的next指針此時還是null!也就是說,會出現後續節點被漏掉的情況。

  2. 圖解流程

    線程A執行CAS将目前節點置為尾部:

  3. 原本線程A要執行t.next = node;将node2的next設定為node3,但是,此時發生上下文切換,時間片交由線程B,也就是說,此時node2的next還是null

    線程B執行enq邏輯,最終CLH隊列如圖所示:

  4. 此時發生上下文切換,時間片交由線程C,線程C調用了unparkSuccessor方法,假如是從頭到尾的周遊形式,在node2就會發現,next指針為null,似乎沒有後續節點了。

    此時發生上下文切換,時間片交由線程A,A将node2的next=node3。奇怪的現象發生了:對于線程C來說,後續沒有node3和node4,但是對于其它線程來說,卻出現了這兩個節點

  5. 結尾

    從頭部周遊會出現這種問題的原因我們找到了,最後我們再來說說為什麼從尾部周遊不會出現這種問題呢?

    其最根本的原因在于:

    node.prev = t;先于CAS執行,也就是說,你在将目前節點置為尾部之前就已經把前驅節點指派了,自然不會出現prev=null的情況