從JDK原始碼角度看併發鎖的優化

超人汪小建發表於2017-05-04

在CLH鎖核心思想的影響下,JDK併發包以CLH鎖作為基礎而設計,其中主要是考慮到CLH鎖更容易實現取消與超時功能。比起原來的CLH鎖已經做了很大的改造,主要從兩方面進行了改造:節點的結構與節點等待機制。

在結構上引入了頭結點和尾節點,他們分別指向佇列的頭和尾,嘗試獲取鎖、入佇列、釋放鎖等實現都與頭尾節點相關,並且每個節點都引入前驅節點和後後續節點的引用;在等待機制上由原來的自旋改成阻塞喚醒。如圖,通過前驅後續節點的引用一節節連線起來形成一個連結串列佇列,對於頭尾節點的更新必須是原子的。下面詳細看看入隊、檢測掛起、釋放出隊、超時、取消等操作。

從JDK原始碼角度看併發鎖的優化

入隊

整塊邏輯其實是用一個無限迴圈進行CAS操作,即用自旋方式競爭直到成功。將尾節點tail的舊值賦予新節點node的前驅節點,並嘗試CAS操作將新節點node賦予尾節點tail,原先的尾節點的後續節點指向新建節點node。完成上面步驟就建立起一條如圖所示的連結串列佇列。程式碼簡化如下:

for (;;) {  
    Node t = tail;  
    node.prev = t;  
    if (compareAndSetTail(t, node)) {  
        t.next = node;  
        return node;  
    }  
}複製程式碼

檢測掛起

上面我們說到節點等待機制已經被JDK併發作者由自旋機制改造成阻塞機制,一個新建的節點完成入隊操作後,如果是自旋則直接進入迴圈檢測前驅節點是否為頭結點即可,但現在被改為阻塞機制,當前執行緒將首先檢測是否為頭結點且嘗試獲取鎖,如果當前節點為頭結點併成功獲取鎖則直接返回,當前執行緒不進入阻塞,否則將當前執行緒阻塞。程式碼簡化如下:

for (;;) {  
    if (node.prev == head)  
        if(嘗試獲取鎖成功){  
             head=node;  
             node.next=null;  
             return;  
         }  
   阻塞執行緒  
}複製程式碼

釋放出隊

出隊的主要工作是負責喚醒等待佇列中後續節點,讓所有等待節點環環相接,每條執行緒有序地往下執行。程式碼簡化如下:

Node s = node.next;
喚醒節點s包含的執行緒複製程式碼

超時

在支援超時的模式下需要LockSupport類的parkNanos方法支援,執行緒在阻塞一段時間後會自動喚醒,每次迴圈將累加消耗時間,當總消耗時間大於等於自定義的超時時間時就直接分返。程式碼簡化如下:

for (;;) {  
    嘗試獲取鎖  
    if (nanosTimeout <= 總消耗時間)  
        return;  
    LockSupport.parkNanos(this, nanosTimeout);  
 }複製程式碼

取消

佇列中等待鎖的佇列可能因為中斷或超時而涉及到取消操作,這種情況下被取消的節點不再進行鎖競爭。此過程主要完成的工作是將取消的節點移除,先將節點的。先將節點node狀態設定成取消,再將前驅節點pred的後續節點指向node的後續節點,這裡由於涉及到競爭,必須通過CAS進行操作,CAS操作就算失敗也不必理會,因為已經改了節點的狀態,在嘗試獲取鎖操作中會迴圈對節點的狀態判斷。

node.waitStatus = Node.CANCELLED;  
Node pred = node.prev;  
Node predNext = pred.next;  
Node next = node.next;  
compareAndSetNext(pred, predNext, next);複製程式碼

====廣告時間,可直接跳過====

鄙人的新書《Tomcat核心設計剖析》已經在京東預售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

=========================

歡迎關注:

從JDK原始碼角度看併發鎖的優化

相關文章