JAVA同步器——ReentrantLock

清晰的記錄,不辜負此刻的青春,日後的完善,成就更美好的未來


  •  concurrent包結構

最底層:

volatile變量:volatile保證變量在內存中的可見性。java線程模型包括線程的私有內存和所有線程的公共內存,這與cpu緩存和內存類似。線程對變量進行操作的時候一般是從公共內存中拷貝變量的副本,等修改完之後重新寫入到公共內存,這存在併發風險。而被volatile標註的變量通過CPU原語保證了變量的內存可見性,也就是說一個線程讀到工作內存中的變量一定是其他線程已經更新到內存中的變量。爲簡單起見,你可以想象成所有線程都在公共內存中操作volatile變量,這樣一個線程對volatile的更改其他線程都看的見,即內存可見性!

CAS:compare and swap,比較-相等-替換新值,跟底層CPU有關,一般CAS操作的都是volatile變量。CAS操作包含內存位置(V)、預期原值(A)和新值(B), 如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。否則,處理器不做任何操作。如方法a.CAS(b,c),如果內存中a與預期值b相等,那麼把a更新成c。cas操作在java中由sun.misc.Unsafe類全權代理了!

最底層結:concurrent包中用cas死循環修改volatile變量直到修改成功是最常見的手法!

第二層:

AtomicXXX類:常用基本類型和引用都有Atomic實現,其中最重要的兩點當然是一個volatile變量和一個CAS操作。++i和i++操作就是volatile和cas的典型用法,incrementAndget和getAndincrement是兩個實現方法,死循環:首先獲取volatile變量值a,然後執行a.cas(a,a+1)直到修改成功。

AQS框架:總有面試官會問你AQS框架,儘管我懷疑他們並不真正瞭解所有細節,Doug lea最屌的思想應該都在AQS框架裏了,直接上圖(借用,侵刪)


該類最主要的兩部分就是state狀態和Node節點(即隊列中的元素),前一個是資源狀態,後一個還是cas操作volatile的典型應用,Node還分爲獨佔模式和共享模式,Node節點的waiteState表示線程的狀態

  •  lock與sychronized的區別

 

     

 

  • lock源碼分析

     lock鎖是再java.util.concurrent.locks包下面的接口,它的子類實現了很多不同的鎖,如ReentrantLock,ReadWriterLock

(實現類ReentrantReadWriteLock),它們都依賴java.utils.concurrent.locks包下的AbstractQueenSynchronizer,簡稱AQS,是lock實現鎖機制的核心;

針對lock的鎖,它分爲

  • 獨佔鎖  共享鎖
  • 公平鎖  非公平鎖  重入鎖
  • 條件鎖  讀寫鎖

ReentrantLock可重入鎖源碼分析

ReentrantLock相當於對sychronized一個實現,他與sychronized一樣是一個獨佔鎖,且是可重入鎖,但是sychronized是一個非公平鎖,對於競態條件下的線程都可能得到該鎖,ReentrantLock可以是公平鎖,也可以是非公平鎖,默認的是非公平鎖;

 

從代碼可以看出ReentrantLock實現了lock接口,並且內部維護了一個靜態內部抽象類Sync,該內部類實現了AQS,而Sync又有倆個子類

非公平鎖子類

公平鎖子類

它們倆個實現大致相同,差別不大,ReentrantLock默認的採用非公平鎖,非公平鎖相對公平鎖而言吞吐量較大,我們從非公平鎖上分析源碼

本文從ReentrantLock加鎖到解鎖源碼流程來分析,ReentrantLock類碼源碼如圖;

從圖中可以看到,該類實現了AQS接口,AQS使用了一個整形volatile變量state來維護同步狀態,這個state變量是所有實現lock的關鍵,加鎖,解鎖都是依靠這個state的狀態位的值去實現;

下面來分析,ReentrantLock的非公平鎖加鎖lock()方法分析

      1.加鎖lock()

 

公平鎖加鎖如下

非公平鎖與公平鎖的區別的是在於,非公平鎖會使用CAS算法去嘗試修改一次AQS字段的state變量,將其從0(無鎖狀態)置爲1(有鎖狀態),(注:ReentrantLock用state表示“持有鎖的線程已經重複獲取該鎖的次數”。當state等於0時,表示當前沒有線程持有鎖)如果成功,執行setExclusiveOwnerThread方法將持有鎖的線程(ownerThread)設置爲當前線程,否則就執行acquire方法,而公平鎖線程不會嘗試去獲取鎖,直接執行acquire方法。

2.acquire方法

acquire方法獲取獨佔模式,忽略中斷,通過調用至少一次tryAcquire方法,成功則返回,否則,線程可能排隊,重複阻塞和接觸阻塞,調用tryAcquire直到成功。

acquire方法的執行邏輯爲:首先調用tryAcquire嘗試獲取鎖,如果獲取不到,則調用addWaiter方法將當前線程加入包裝爲Node對象加入隊列隊尾,之後調用acquireQueued方法不斷的自旋獲取鎖。

其中tryAcquire方法、addWaiter方法、acquireQueued方法我們接下來逐個分析。

3.tryAcquire

非公平鎖的tryAcquire 直接調用Sync的nonfairTryAcquire方法

nofairTryAcquire方法的邏輯:

  •   獲取當前的鎖標記位state,如果state爲0表名此時沒有線程佔用鎖,直接進入if中獲取鎖的邏輯,與非公平鎖lock方法的前半部分一樣,將state標記CAS改變爲1,設置獲取獨佔鎖的線程爲當前線程。
  •  獲取當前的鎖標記位state,如果state爲0表名此時沒有線程佔用鎖,直接進入if中獲取鎖的邏輯,與非公平鎖lock方法的前半部分一樣,將state標記CAS改變爲1,設置獲取獨佔鎖的線程爲當前線程。
  •  如果state鎖標記位既不爲0,也不是當前線程,表示其他線程來爭奪鎖,結果當然是失敗。

我們回到第二步,當tryAcquire方法返回true,說明沒有線程佔用鎖,當前線程獲取鎖成功,後續的addWaiter方法與acquireQueued方法不再執行並返回,線程執行同步塊中的方法。若tryAcquire方法返回false,說明當前有其他線程佔用鎖,此時將會觸發執行addWaiter方法與acquireQueued方法

公平鎖中的tryAcquire方法與非公平鎖基本相同,只不過比非公平鎖在第一次獲取鎖時的判斷中多了hasQueuedPredecessors方法

hasQueuedPredecessors用於判斷

前線程是否爲head節點的後續節點線程(預備獲取鎖的線程節點)。

或者說:判斷“當前線程”是不是CLH隊列中的第一個線程(head節點的後置節點),若是的話返回false,不是返回true。

4.addWaiter方法

addWaiter方法的主要目的是將當前線程包裝爲一個獨佔模式的Node隱式隊列,在分析方法前我們需要了解Node類的幾個重要的參數:

prev:前置節點

next:後置節點

wiatStatus:是等待鏈表(隊列)中的狀態,狀態分一下幾種

在AQS中,記錄了head節點和tail節點

 

首先將當前線程保障成一個獨佔模式的Node節點對象,然後判斷當前隊尾(tail節點)是否有節點,如果有,則通過CAS將隊尾節點設置爲當前節點,並將當前節點的前置節點設置爲上一個尾節點。

如果tail尾節點爲null,說明當前節點爲第一個入隊節點,或者CAS設置當前節點爲尾節點失敗,將調用enq方法。

 

enq方法也是一個CAS方法,當第一次循環tail尾節點爲null時,說明當前節點爲第一個入隊節點,此時將新建一個空Node節點爲傀儡節點,並將其設置爲隊首,然後再次循環時,將當前節點設置爲tail尾節點,失敗將循環設定直至成功。若是addWaiter方法中設置tail尾節點失敗的話,進入enq方法後直接將進入else模塊將當前節點設置爲tail尾節點,循環設定直至成功。

接了方便理解addWaiter方法的作用,以及後續acquireQueued的理解,我們通過3個線程來畫圖演示從第1步到第4步AQS中Node隊列的情況:

     1.假設當前有一個線程 thread-1執行lock方法,由於此時沒有其他線程佔用鎖,thread-1得到了鎖

     2.此時thread-2執行lock方法,由於此時thread-1佔用鎖,因此thread-2執行acquire方法,並且thread-1不釋放鎖,tryAcquire方法失敗,執行addWaiter方法

由於thread-2第一個進入隊列,此時AQS中head以及tail爲null,因此進入執行enq方法,根據上面描述的enq方法邏輯,執行之後等待隊列爲

3.接下來thread-3執行lock方法,thread-1依然沒有釋放鎖,此時對接就變成這樣

addWaiter方法的的作用就是將一個個沒有獲取鎖的線程,包裝成爲一個等待隊列。

5.acquireQueued方法

acquireQueued方法的作用就是CAS循環獲取鎖的方法,並且如果當前節點爲head節點的後續節點,則嘗試獲取鎖,如果獲取成功則將當前節點置爲head節點,並返回,如果獲取失敗或者當前節點並不是head節點的後續節點,則調用shouldParkAfterFailedAcquire方法以及parkAndCheckInterrupt方法,將當前節點的前置節點狀態位置爲SIGNAL(-1) ,並阻塞當前節點。

6.shouldParkAfterFailedAcquire方法

 /
     * @param pred 前繼節點
     * @param node 當前節點
      /
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)

        /*
         * 
         * 前繼節點還在等待觸發,還沒當前節點的什麼事兒,所以當前節點可以被park
         */
        return true;
    if (ws > 0) {
        /*
         * 前繼節點是CANCELLED ,則需要充同步隊列中刪除,並檢測新接上的前繼節點的狀態,若還是爲CANCELLED ,還需要重複上述步驟
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 到這一步,waitstatus只有可能有2種狀態,一個是0,一個是PROPAGATE,無論是哪個都需要把當前節點的狀態設置爲SIGNAL
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

 

shouldParkAfterFailedAcquire方法根據源碼分許我們可以得知,該方法就是用來設置前置節點的狀態位爲SIGNAL(-1),而SIGNAL(-1)表示節點的後置節點處於阻塞狀態。首次進入該方法時,前置節點的waitStatus爲0,因此進入else代碼塊中,通過CAS將waitStatus設置爲-1,當外圍方法acquireQueued再次循環時,將直接返回true。這時將滿足判斷條件執行parkAndCheckInterrupt方法。

而中間這塊判斷邏輯則是前置節點狀態爲CANCELLED(1),則繼續查找前置節點的前驅節點,因爲當head節點喚醒時,會跳過CANCELLED(1)節點(CANCELLED(1):因爲超時或中斷或異常,該線程已經被取消)。

摘取網上大神的shouldParkAfterFailedAcquire方法的邏輯總結:

  1. 如果前繼的節點狀態爲SIGNAL,表明當前節點需要unpark,則返回成功,此時acquireQueued方法的第12行(parkAndCheckInterrupt)將導致線程阻塞

  2. 如果前繼節點狀態爲CANCELLED(ws>0),說明前置節點已經被放棄,則回溯到一個非取消的前繼節點,返回false,acquireQueued方法的無限循環將遞歸調用該方法,直至規則1返回true,導致線程阻塞(parkAndCheckInterrupt)

  3. 如果前繼節點狀態爲非SIGNAL、非CANCELLED,則設置前繼的狀態爲SIGNAL,返回false後進入acquireQueued的無限循環,與第2點相同

總體看來,shouldParkAfterFailedAcquire就是靠前繼節點判斷當前線程是否應該被阻塞,如果前繼節點處於CANCELLED狀態,則順便刪除這些節點重新構造隊列。

7.parkAndCheckInterrupt方法

很簡單,就是講將前線程中斷,返回中斷狀態。

那麼此時我們來通過畫圖,總結下步驟5~步驟7:

在thread-2與thread-3執行完步驟4後

此時thread-2執行步驟5,由於他的前置節點爲Head節點因此它有了一次tryAcquire獲取鎖的機會,如果成功則設置thread-2的Node節點爲head節點然後返回,由於當前節點沒有被中斷,因此返回的中斷標記位爲false。

如果tryAcquire獲取鎖依然失敗,則調用shouldParkAfterFailedAcquire方法以及parkAndCheckInterrupt方法對線程進行阻塞,直到持有鎖的線程釋放鎖時被喚醒(具體後續說明,現在只需要知道前置節點獲取釋放鎖後,會喚醒他的後置節點的線程),此時執行了shouldParkAfterFailedAcquire方法後將會變成這樣

當鎖被釋放thread-2被喚醒後再次執行tryAcquire獲取鎖,此時由於鎖已釋放獲取將會成功,但由於當前節點被中斷過interrupted爲true,因此返回的中斷標記位爲true。

回到上面步驟2中,此時將會執行selfInterrupt方法將當前線程從阻塞狀態喚醒。

而thread-3則和thread-2經歷差不多,區別在於thread-3的前置節點不是head節點,因此進入acquireQueued方法後thread-3直接被阻塞,直到thread-2獲取鎖後變爲head節點並且釋放鎖之後,thread-3纔會被喚醒。thread-3進入acquireQueued方法後變爲

(爲了避免大家理解不了,此處再次說明,前置節點的waitStatus爲-1時表示當前節點處於阻塞態)

下面來說說步驟5中acquireQueued方法的finally代碼塊

cancelAcquire方法:如果出現異常或者出現中斷,就會執行finally的取消線程的請求操作,核心代碼是node.waitStatus = Node.CANCELLED;將線程的狀態改爲CANCELLED。

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // 獲取前置節點並判斷狀態,如果前置節點已被取消,則將其丟棄重新指向前置節點,直到指向一個距離當前節點最近的有效節點,這種處理非常巧妙讓人佩服
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    //獲取新的前置節點的後置節點(此時新的前置節點的next還沒有指向當前節點)
    Node predNext = pred.next;

    //將當前節點設置爲取消狀態
    node.waitStatus = Node.CANCELLED;

    // 如果當前節點爲尾部節點,直接丟棄
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        //如果前置節點不是head,則後置節點需要一個狀態,來對標記當前節點的狀態,此處是設置新的前置節點的waitStatus爲SIGNAL(-1),並且將新的前置節點的next指向當前節點,當前節點不會再此處被丟棄,而是在shouldParkAfterFailedAcquire方法中丟棄
        int ws;
        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 {
            //如果前置節點爲head,則直接喚醒距離當前節點最近的有效後置節點
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

unparkSuccessor方法源碼如下:

首先,將當前節點waitStatus設置爲0,然後獲取距離當前節點最近的有效後置節點,最後unpark喚醒後置節點的線程,此時後置節點線程就有機會獲取鎖了

至此,所有的lock邏輯全部走完了,下面來說說解鎖。

ReentrantLock的unlock方法

其實是調用的AQS的release方法

release方法的邏輯爲,首先調用tryRelease方法,如果返回true,就執行unparkSuccessor方法喚醒後置節點線程。接下來我們看看tryRelease方法,在ReentrantLock中實現。

我們看到在tryRelease方法中首先會獲取state鎖標記,將其進行-1操作,並且返回結果根據state鎖標記位是否爲0,如果爲0則返回true,否則返回false。

我們知道ReentrantLock是一個可重入鎖,前面分析了同一個線程,每次獲取鎖,重入鎖,都會爲state鎖標記+1,state記錄了線程獲取了多少次鎖。那麼同一個線程獲取了多少次鎖,就要進行多少次解鎖,直到全部解鎖,state鎖標記爲0時,表示解鎖成功,tryRelease方法返回true,後續喚醒後置節點線程。

 

網上找倆張圖,幫助加深理解

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章