Synchronized,Lock底層解析

概述

        在JDK1.5之前,使用synchronized來實現線程同步的,同步的開銷較大效率較低,因此在JDK1.5之後,推出了代碼層面的Lock接口(synchronized爲jvm層面)來實現與synchronized同樣功能的同步鎖功能。

        在java.util.concurrent.locks包中有很多Lock的實現類,常用的有ReentrantLock,其實現都依AbstractQueuedSynchronizer類簡稱AQS,他是實現Lock接口所有鎖的核心。

Synchronized與Lock的區別

類別 synchronized Lock
存在層次 Java關鍵字,在JVM層面 一個類
鎖的釋放

1、以獲取鎖的線程執行完同步代碼,釋放鎖

2、線程執行發生異常,jvm會讓線程釋放鎖

在finally中必須釋放鎖,不然容易造成線程死鎖
鎖的獲取 假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待 公平鎖和非公平鎖(多個可以競爭,默認方式)
鎖狀態 無法判斷 可以判斷
鎖類型 可重入 不可中斷 非公平 可重入 可中斷 可公平/非公平(兩者皆可)
性能 少量同步 大量同步

synchronized鎖源碼分析

       它的實現時基於jvm指令去實現的下面我們寫一個簡單的synchronized示例。

       通過反編譯,可以看到如下: 

       如上就是這段代碼段字節碼指令,我們可以清晰段看到,其實synchronized映射成字節碼指令就是增加來兩個指令:monitorenter和monitorexit。我們看到上面的class文件第9、13、19行,分別加入了monitorenter、monitorexit、monitorexit。當一條線程進行執行的遇到monitorenter指令的時候,它會去嘗試獲得鎖,如果獲得鎖那麼鎖計數+1(因爲它是一個可重入鎖,所以需要用這個鎖計數判斷鎖的情況),如果沒有獲得鎖,那麼阻塞。當它遇到monitorexit的時候,鎖計數器-1,當計數器爲0,那麼就釋放鎖。

       怎麼會有2個monitorexit?因爲synchronized鎖釋放有兩種機制,一種就是執行完釋放;另外一種就是發送異常,虛擬機釋放。圖中第二個monitorexit就是發生異常時執行的流程。而且,從圖中我們也可以看到在第14行,有一個goto指令,也就是說如果正常運行結束會跳轉到22行執行。

Lock接口源碼逐個分析

針對Lock接口中的鎖,我們來一個個來深入分析,首先需要了解下面的名詞概念

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

ReentrantLock 可重入鎖深入源碼分析

ReentrantLock相當於是對synchronized的一個實現,他與synchronized一樣是一個可重入鎖並且是一個獨佔鎖,但是synchronized是一個非公平鎖,任何處於競爭隊列的線程都有可能獲取鎖,而ReentrantLock既可以爲公平鎖,又可以爲非公平鎖。

根據上面的源碼我們可知,ReentrantLock繼承於Lock接口,並且是併發編程大師Doug Lea所創作(向大師致敬)並且在源碼中我們可以發現,很多操作都是基於Sync這個類實現的,而Sync是一個內部抽象靜態類,繼承AQS類。而Sync又有兩個子類:

非公平鎖子類:

static final class NonfairSync extends Sync

公平鎖子類:

static final class FairSync extends Sync

他們兩個的實現大致相同,差別不大,並且ReentrantLock的默認是採用非公平鎖,非公平鎖相對公平鎖而言在吞吐量上有較大優勢,我們分析源碼也主要從非公平鎖入手。

本人主要通過ReentrantLock的加鎖,到解鎖的源碼流程來分析。ReentrantLock的類圖如下

ReentrantLock的實現依賴於Java同步器框架AbstractQueuedSynchronizer(本文簡稱之爲AQS)。AQS使用一個整型的volatile變量(命名爲state)來維護同步狀態,馬上我們會看到,這個volatile變量是所有Lock實現鎖的關鍵。加鎖、解鎖的狀態全都圍繞這個狀態位去實現。

閒話不多說,首先是ReentrantLock的非公平鎖的加鎖方法lock()

1.第一步,加鎖lock()

公平鎖的加鎖方法lock()如下

我們可以看到非公平鎖與公平鎖的加鎖區別在於,非公平鎖首先會進行一次CAS,去嘗試修改AQS中的鎖標記state字段,將其從0(無鎖狀態),修改爲1(鎖定狀態)(注:ReentrantLock用state表示“持有鎖的線程已經重複獲取該鎖的次數”。當state等於0時,表示當前沒有線程持有鎖),如果成功,就設置ExclusiveOwnerThread的值爲當前線程(Exclusive是獨佔的意思,ReentrantLock用exclusiveOwnerThread表示“持有鎖的線程”

,如果成功,執行setExclusiveOwnerThread方法將持有鎖的線程(ownerThread)設置爲當前線程,否則就執行acquire方法,而公平鎖線程不會嘗試去獲取鎖,直接執行acquire方法。

2.acquire方法

根據acquire方法的註釋大概能知道他的作用:

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

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

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

3.tryAcquire方法

非公平鎖中的tryAcquire方法直接調用Sync的nofairTryAcquire方法,源碼如下:

nofairTryAcquire方法的邏輯:

  • 獲取當前的鎖標記位state,如果state爲0表名此時沒有線程佔用鎖,直接進入if中獲取鎖的邏輯,與非公平鎖lock方法的前半部分一樣,將state標記CAS改變爲1,設置獲取獨佔鎖的線程爲當前線程。
  • 如果state鎖標記位的state不爲0,說明有線程佔用了該鎖,此時需要判斷佔用鎖的線程是否爲當前線程(getExclusiveQwnerThread方法獲取佔用鎖的線程),如果是當前線程,則將state鎖標記位+1 表示重入,並修改status值,但因爲沒有競爭,獲取鎖的線程還是當前線程,所以通過setStatus修改,而非CAS,也就是說這段代碼實現了類似偏向鎖的功能,並且實現的非常漂亮。
  • 如果state鎖標記位既不爲0,也不是當前線程,表示其他線程來爭奪鎖,結果當然是失敗。

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

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

hasQueuedPredecessors方法用於判斷當前線程是否爲head節點的後續節點線程(預備獲取鎖的線程節點)。

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

說明: 通過代碼,能分析出,hasQueuedPredecessors() 是通過判斷"當前線程"是不是在CLH隊列的隊首,來返回AQS中是不是有比“當前線程”等待更久的線程。下面對head、tail和Node進行說明。

4.addWaiter方法

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

prev:前置節點;

next:後置節點;

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

而在AQS中,記錄了Node的頭結點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方法

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,後續喚醒後置節點線程。

至此ReentrantLock源碼分析完畢,其他鎖待續。。。

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