在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 |
---|---|---|
存在層次 | Java的關鍵字,在jvm層面上 | 是一個類 |
鎖的釋放 | 一、以獲取鎖的線程執行完同步代碼,釋放鎖node 二、線程執行發生異常,jvm會讓線程釋放鎖編程 |
在finally中必須釋放鎖,否則容易形成線程死鎖 |
鎖的獲取 | 假設A線程得到鎖,B線程等待。若是A線程阻塞,B線程會一直等待 | 分狀況而定,Lock有多個鎖獲取的方式,具體下面會說道,大體就是能夠嘗試得到鎖,線程能夠不用一直等待 |
鎖狀態 | 沒法判斷 | 能夠判斷 |
鎖類型 | 可重入 不可中斷 非公平 | 可重入 可中斷 可公平/非公平(二者皆可) |
性能 | 少許同步 | 大量同步 |
對於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接口中的鎖,咱們來一個個來深刻分析,首先須要瞭解下面的名詞概念性能
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方法的邏輯:
咱們回到第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方法的邏輯總結:
若是前繼的節點狀態爲SIGNAL,代表當前節點須要unpark,則返回成功,此時acquireQueued方法的第12行(parkAndCheckInterrupt)將致使線程阻塞
若是前繼節點狀態爲CANCELLED(ws>0),說明前置節點已經被放棄,則回溯到一個非取消的前繼節點,返回false,acquireQueued方法的無限循環將遞歸調用該方法,直至規則1返回true,致使線程阻塞(parkAndCheckInterrupt)
若是前繼節點狀態爲非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