先來熟悉一下代碼,挂起和喚醒這兩部分
- 尾部周遊源碼
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循環中的邏輯:從尾部開始向前周遊,找到最前的一個處于正常阻塞狀态的結點,直到節點重合(即等于目前節點)
-
高并發下入隊邏輯
既然采用了從尾部周遊的邏輯,那麼肯定是為了解決可能會出現的問題。而這個問題就在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;
}
}
}
}
-
原子性問題
在該段方法中,将目前節點置于尾部使用了CAS來保證線程安全,但是請注意:在if語句塊中的代碼并沒有使用任何手段來保證線程安全!
也就是說,在高并發情況下,可能會出現這種情況:
線程A通過CAS進入if語句塊之後,發生上下文切換,此時線程B同樣執行了該方法,并且執行完畢。然後線程C調用了unparkSuccessor方法。
假如是從頭到尾的周遊形式,線程A的next指針此時還是null!也就是說,會出現後續節點被漏掉的情況。
-
圖解流程
線程A執行CAS将目前節點置為尾部:
-
原本線程A要執行t.next = node;将node2的next設定為node3,但是,此時發生上下文切換,時間片交由線程B,也就是說,此時node2的next還是null
線程B執行enq邏輯,最終CLH隊列如圖所示:
-
此時發生上下文切換,時間片交由線程C,線程C調用了unparkSuccessor方法,假如是從頭到尾的周遊形式,在node2就會發現,next指針為null,似乎沒有後續節點了。
此時發生上下文切換,時間片交由線程A,A将node2的next=node3。奇怪的現象發生了:對于線程C來說,後續沒有node3和node4,但是對于其它線程來說,卻出現了這兩個節點
-
結尾
從頭部周遊會出現這種問題的原因我們找到了,最後我們再來說說為什麼從尾部周遊不會出現這種問題呢?
其最根本的原因在于:
node.prev = t;先于CAS執行,也就是說,你在将目前節點置為尾部之前就已經把前驅節點指派了,自然不會出現prev=null的情況