Synchronized與Lock的底層實現解析

在JDK1.5以前,咱們在編寫併發程序的時候無一例外都是使用synchronized來實現線程同步的,而synchronized在JDK1.5以前同步的開銷較大效率較低,所以在JDK1.5以後,推出了代碼層面的Lock接口(synchronized爲jvm層面)來實現與synchronized一樣功能的同步鎖,而且針對不一樣的併發場景也加入了許多個性鎖功能。html

在java.util.concurrent.locks包中有不少Lock的實現類,經常使用的有ReentrantLock、ReadWriteLock(實現類ReentrantReadWriteLock),其實現都依賴java.util.concurrent.AbstractQueuedSynchronizer類簡稱AQS,他是實現Lock接口全部鎖的核心。java

Synchronized與Lock的區別

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

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

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

在finally中必須釋放鎖,否則容易形成線程死鎖
鎖的獲取 假設A線程得到鎖,B線程等待。若是A線程阻塞,B線程會一直等待 分狀況而定,Lock有多個鎖獲取的方式,具體下面會說道,大體就是能夠嘗試得到鎖,線程能夠不用一直等待
鎖狀態 沒法判斷 能夠判斷
鎖類型 可重入 不可中斷 非公平 可重入 可中斷 可公平/非公平(二者皆可)
性能 少許同步 大量同步

synchronized鎖源碼分析

對於synchronized來講,其實沒有什麼所謂的源碼去分析,synchronized是Java中的一個關鍵字,他的實現時基於jvm指令去實現的下面咱們寫一個簡單的synchronized示例併發

咱們點擊查看SyncDemo.java的源碼SyncDemo.class,能夠看到以下: 
框架

如上就是這段代碼段字節碼指令,沒你想的那麼難吧。言歸正傳,咱們能夠清晰段看到,其實synchronized映射成字節碼指令就是增長來兩個指令:monitorenter和monitorexit。咱們看到上面的class文件第九、1三、19行,分別加入了monitorenter、monitorexit、monitorexit。當一條線程進行執行的遇到monitorenter指令的時候,它會去嘗試得到鎖,若是得到鎖那麼鎖計數+1(爲何會加一呢,由於它是一個可重入鎖,因此須要用這個鎖計數判斷鎖的狀況),若是沒有得到鎖,那麼阻塞。當它遇到monitorexit的時候,鎖計數器-1,當計數器爲0,那麼就釋放鎖。jvm

那麼有的朋友看到這裏就疑惑了,那圖上有2個monitorexit呀?立刻回答這個問題:上面我之前寫的文章也有表述過,synchronized鎖釋放有兩種機制,一種就是執行完釋放;另一種就是發送異常,虛擬機釋放。圖中第二個monitorexit就是發生異常時執行的流程,這就是我開頭說的「會有2個流程存在「。並且,從圖中咱們也能夠看到在第14行,有一個goto指令,也就是說若是正常運行結束會跳轉到22行執行。源碼分析

Lock接口源碼逐個分析

針對Lock接口中的鎖,咱們來一個個來深刻分析,首先須要瞭解下面的名詞概念性能

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

ReentrantLock 可重入鎖深刻源碼分析

ReentrantLock至關因而對synchronized的一個實現,他與synchronized同樣是一個可重入鎖而且是一個獨佔鎖,可是synchronized是一個非公平鎖,任何處於競爭隊列的線程都有可能獲取鎖,而ReentrantLock既能夠爲公平鎖,又能夠爲非公平鎖。ui

根據上面的源碼咱們可知,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源碼分析完畢,其餘鎖待續。。。

參考:

https://blog.csdn.net/wangxiaotongfan/article/details/51800981

https://www.cnblogs.com/sheeva/p/6472949.html

http://www.cnblogs.com/lcchuguo/p/5036172.html

https://www.jianshu.com/p/e4301229f59e

https://blog.csdn.net/mayongzhan_csdn/article/details/79374996

https://blog.csdn.net/Luxia_24/article/details/52403033

https://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1

相關文章
相關標籤/搜索