ReentrantLock源碼分析從入門到入土

NO IMAGE

回答一個問題

在開始本篇文章的內容講述前,先來回答我一個問題,為什麼 JDK 提供一個 synchronized 關鍵字之後還要提供一個 Lock 鎖,這不是多此一舉嗎?難道 JDK 設計人員都是沙雕嗎?

我聽過一句話非常的經典,也是我認為是每個人都應該瞭解的一句話:你以為的並不是你以為的。明白什麼意思麼?不明白的話,加我微信我告訴你。

初識 ReentrantLock

ReentrantLock 位於 java.util.concurrent.locks 包下,它實現了 Lock 接口和 Serializable 接口。

ReentrantLock源碼分析從入門到入土

ReentrantLock 是一把可重入鎖互斥鎖,它具有與 synchronized 關鍵字相同的含有隱式監視器鎖(monitor)的基本行為和語義,但是它比 synchronized 具有更多的方法和功能。

ReentrantLock 基本方法

構造方法

ReentrantLock 類中帶有兩個構造函數,一個是默認的構造函數,不帶任何參數;一個是帶有 fair 參數的構造函數

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

第二個構造函數也是判斷 ReentrantLock 是否是公平鎖的條件,如果 fair 為 true,則會創建一個公平鎖的實現,也就是 new FairSync(),如果 fair 為 false,則會創建一個 非公平鎖的實現,也就是 new NonfairSync(),默認的情況下創建的是非公平鎖

// 創建的是公平鎖
private ReentrantLock lock = new ReentrantLock(true);
// 創建的是非公平鎖
private ReentrantLock lock = new ReentrantLock(false);
// 默認創建非公平鎖
private ReentrantLock lock = new ReentrantLock();

FairSync 和 NonfairSync 都是 ReentrantLock 的內部類,繼承於 Sync 類,下面來看一下它們的繼承結構,便於梳理。

ReentrantLock源碼分析從入門到入土

abstract static class Sync extends AbstractQueuedSynchronizer {...}
static final class FairSync extends Sync {...}
static final class NonfairSync extends Sync {...}

在多線程嘗試加鎖時,如果是公平鎖,那麼鎖獲取的機會是相同的。否則,如果是非公平鎖,那麼 ReentrantLock 則不會保證每個鎖的訪問順序

下面是一個公平鎖的實現

public class MyFairLock extends Thread{
private ReentrantLock lock = new ReentrantLock(true);
public void fairLock(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()  + "正在持有鎖");
}finally {
System.out.println(Thread.currentThread().getName()  + "釋放了鎖");
lock.unlock();
}
}
public static void main(String[] args) {
MyFairLock myFairLock = new MyFairLock();
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "啟動");
myFairLock.fairLock();
};
Thread[] thread = new Thread[10];
for(int i = 0;i < 10;i++){
thread[i] = new Thread(runnable);
}
for(int i = 0;i < 10;i++){
thread[i].start();
}
}
}

不信?不信你輸出試試啊!懶得輸出?就知道你懶得輸出,所以直接告訴你結論吧,結論就是自己試

試完了嗎?試完了我是不會讓你休息的,過來再試一下非公平鎖的測試和結論,知道怎麼試嗎?上面不是講過要給 ReentrantLock 傳遞一個參數的嗎?你想,傳 true 的時候是公平鎖,那麼反過來不就是非公平鎖了?其他代碼還用改嗎?不需要了啊。

明白了吧,再來測試一下非公平鎖的流程,看看是不是你想要的結果。

公平鎖的加鎖(lock)流程詳解

通常情況下,使用多線程訪問公平鎖的效率會非常低(通常情況下會慢很多),但是 ReentrantLock 會保證每個線程都會公平的持有鎖,線程飢餓的次數比較小。鎖的公平性並不能保證線程調度的公平性。

此時如果你想了解更多的話,那麼我就從源碼的角度跟你聊聊如何 ReentrantLock 是如何實現這兩種鎖的。

ReentrantLock源碼分析從入門到入土

如上圖所示,公平鎖的加鎖流程要比非公平鎖的加鎖流程簡單,下面要聊一下具體的流程了,請小夥伴們備好板凳。

下面先看一張流程圖,這張圖是 acquire 方法的三條主要流程

ReentrantLock源碼分析從入門到入土

首先是第一條路線,tryAcquire 方法,顧名思義嘗試獲取,也就是說可以成功獲取鎖,也可以獲取鎖失敗。

使用 ctrl+左鍵 點進去是調用 AQS 的方法,但是 ReentrantLock 實現了 AQS 接口,所以調用的是 ReentrantLock 的 tryAcquire 方法;

ReentrantLock源碼分析從入門到入土

首先會取得當前線程,然後去讀取當前鎖的同步狀態,還記得鎖的四種狀態嗎?分別是 無鎖、偏向鎖、輕量級鎖和重量級鎖,如果你不是很明白的話,請參考博主這篇文章(不懂什麼是鎖?看看這篇你就明白了),如果判斷同步狀態是 0 的話,就證明是無鎖的,參考下面這幅圖( 1bit 表示的是是否偏向鎖 )

ReentrantLock源碼分析從入門到入土

如果是無鎖(也就是沒有加鎖),說明是第一次上鎖,首先會先判斷一下隊列中是否有比當前線程等待時間更長的線程(hasQueuedPredecessors);然後通過 CAS 方法原子性的更新鎖的狀態,CAS 方法更新的要求涉及三個變量,currentValue(當前線程的值),expectedValue(期望更新的值),updateValue(更新的值),它們的更新如下

if(currentValue == expectedValue){
currentValue = updateValue
}

CAS 通過 C 底層機制保證原子性,這個你不需要考慮它。如果既沒有排隊的線程而且使用 CAS 方法成功的把 0 -> 1 (偏向鎖),那麼當前線程就會獲得偏向鎖,記錄獲取鎖的線程為當前線程。

然後我們看 else if 邏輯,如果讀取的同步狀態是1,說明已經線程獲取到了鎖,那麼就先判斷當前線程是不是獲取鎖的線程,如果是的話,記錄一下獲取鎖的次數 + 1,也就是說,只有同步狀態為 0 的時候是無鎖狀態。如果當前線程不是獲取鎖的線程,直接返回 false。

acquire 方法會先查看同步狀態是否獲取成功,如果成功則方法結束返回,也就是 !tryAcquire == false ,若失敗則先調用 addWaiter 方法再調用 acquireQueued 方法

然後看一下第二條路線 addWaiter

ReentrantLock源碼分析從入門到入土

這裡首先把當前線程和 Node 的節點類型進行封裝,Node 節點的類型有兩種,EXCLUSIVESHARED ,前者為獨佔模式,後者為共享模式,具體的區別我們會在 AQS 源碼討論,這裡讀者只需要知道即可。

首先會進行 tail 節點的判斷,有沒有尾節點,其實沒有頭節點也就相當於沒有尾節點,如果有尾節點,就會原子性的將當前節點插入同步隊列中,再執行 enq 入隊操作,入隊操作相當於原子性的把節點插入隊列中。

如果當前同步隊列尾節點為null,說明當前線程是第一個加入同步隊列進行等待的線程。

**在看第三條路線 acquireQueued **

ReentrantLock源碼分析從入門到入土

主要會有兩個分支判斷,首先會進行無限循環中,循環中每次都會判斷給定當前節點的先驅節點,如果沒有先驅節點會直接拋出空指針異常,直到返回 true。

然後判斷給定節點的先驅節點是不是頭節點,並且當前節點能否獲取獨佔式鎖,如果是頭節點並且成功獲取獨佔鎖後,隊列頭指針用指向當前節點,然後釋放前驅節點。如果沒有獲取到獨佔鎖,就會進入 shouldParkAfterFailedAcquireparkAndCheckInterrupt 方法中,我們貼出這兩個方法的源碼

ReentrantLock源碼分析從入門到入土

shouldParkAfterFailedAcquire 方法主要邏輯是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS將節點狀態由 INITIAL 設置成 SIGNAL,表示當前線程阻塞。當 compareAndSetWaitStatus 設置失敗則說明 shouldParkAfterFailedAcquire 方法返回 false,然後會在 acquireQueued 方法中死循環中會繼續重試,直至compareAndSetWaitStatus 設置節點狀態位為 SIGNAL 時 shouldParkAfterFailedAcquire 返回 true 時才會執行方法 parkAndCheckInterrupt 方法。(這塊在後面研究 AQS 會細講)

parkAndCheckInterrupt 該方法的關鍵是會調用 LookSupport.park 方法(關於LookSupport會在以後的文章進行討論),該方法是用來阻塞當前線程。

所以 acquireQueued 主要做了兩件事情:如果當前節點的前驅節點是頭節點,並且能夠獲取獨佔鎖,那麼當前線程能夠獲得鎖該方法執行結束退出

如果獲取鎖失敗的話,先將節點狀態設置成 SIGNAL,然後調用 LookSupport.park 方法使得當前線程阻塞。

如果 !tryAcquireacquireQueued 都為 true 的話,則打斷當前線程。

那麼它們的主要流程如下(注:只是加鎖流程,並不是 lock 所有流程)

ReentrantLock源碼分析從入門到入土

非公平鎖的加鎖(lock)流程詳解

非公平鎖的加鎖步驟和公平鎖的步驟只有兩處不同,一處是非公平鎖在加鎖前會直接使用 CAS 操作設置同步狀態,如果設置成功,就會把當前線程設置為偏向鎖的線程;一處是 CAS 操作失敗執行 tryAcquire 方法,讀取線程同步狀態,如果未加鎖會使用 CAS 再次進行加鎖,不會等待 hasQueuedPredecessors 方法的執行,達到只要線程釋放鎖就會加鎖的目的。下面通過源碼和流程圖來詳細理解

ReentrantLock源碼分析從入門到入土

這是非公平鎖和公平鎖不同的兩處地方,下面是非公平鎖的加鎖流程圖

ReentrantLock源碼分析從入門到入土

lockInterruptibly 以可中斷的方式獲取鎖

以下是 JavaDoc 官方解釋:

lockInterruptibly 的中文意思為如果沒有被打斷,則獲取鎖。如果沒有其他線程持有該鎖,則獲取該鎖並立即返回,將鎖保持計數設置為1。如果當前線程已經持有鎖,那麼此方法會立刻返回並且持有鎖的數量會 + 1。如果鎖是由另一個線程持有的,則出於線程調度目的,當前線程將被禁用,並處於休眠狀態,直到發生以下兩種情況之一

  • 鎖被當前線程持有
  • 一些其他線程打斷了當前線程

如果當前線程獲取了鎖,則鎖保持計數將設置為1。

如果當前線程發生瞭如下情況:

  • 在進入此方法時設置了其中斷狀態
  • 當獲取鎖的時候發生了中斷(Thread.interrupt)

那麼當前線程就會拋出InterruptedException 並且當前線程的中斷狀態會清除。

下面看一下它的源碼是怎麼寫的

ReentrantLock源碼分析從入門到入土

首先會調用 acquireInterruptibly 這個方法,判斷當前線程是否被中斷,如果中斷拋出異常,沒有中斷則判斷公平鎖/非公平鎖 是否已經獲取鎖,如果沒有獲取鎖(tryAcquire 返回 false)則調用 doAcquireInterruptibly 方法,這個方法和 acquireQueued 方法沒什麼區別,就是線程在等待狀態的過程中,如果線程被中斷,線程會拋出異常。

下面是它的流程圖

ReentrantLock源碼分析從入門到入土

tryLock 嘗試加鎖

僅僅當其他線程沒有獲取這把鎖的時候獲取這把鎖,tryLock 的源代碼和非公平鎖的加鎖流程基本一致,它的源代碼如下

ReentrantLock源碼分析從入門到入土

tryLock 超時獲取鎖

ReentrantLock除了能以中斷的方式去獲取鎖,還可以以超時等待的方式去獲取鎖,所謂超時等待就是線程如果在超時時間內沒有獲取到鎖,那麼就會返回false,而不是一直死循環獲取。可以使用 tryLock 和 tryLock(timeout, unit)) 結合起來實現公平鎖,像這樣

if (lock.tryLock() || lock.tryLock(timeout, unit)) {...}

如果超過了指定時間,則返回值為 false。如果時間小於或者等於零,則該方法根本不會等待。

它的源碼如下

ReentrantLock源碼分析從入門到入土

首先需要了解一下 TimeUnit 工具類,TimeUnit 表示給定粒度單位的持續時間,並且提供了一些用於時分秒跨單位轉換的方法,通過使用這些方法進行定時和延遲操作。

toNanos 用於把 long 型表示的時間轉換成為納秒,然後判斷線程是否被打斷,如果沒有打斷,則以公平鎖/非公平鎖 的方式獲取鎖,如果能夠獲取返回true,獲取失敗則調用doAcquireNanos方法使用超時等待的方式獲取鎖。在超時等待獲取鎖的過程中,如果等待時間大於應等待時間,或者應等待時間設置不合理的話,返回 false。

ReentrantLock源碼分析從入門到入土

這裡面以超時的方式獲取鎖也可以畫一張流程圖如下

ReentrantLock源碼分析從入門到入土

unlock 解鎖流程

unlocklock 是一對情侶,它們分不開彼此,在調用 lock 後必須通過 unlock 進行解鎖。如果當前線程持有鎖,在調用 unlock 後,count 計數將減少。如果保持計數為0就會進行解鎖。如果當前線程沒有持有鎖,在調用 unlock 會拋出 IllegalMonitorStateException 異常。下面是它的源碼

ReentrantLock源碼分析從入門到入土

在有了上面閱讀源碼的經歷後,相信你會很快明白這段代碼的意思,鎖的釋放不會區分公平鎖還是非公平鎖,主要的判斷邏輯就是 tryRelease 方法,getState 方法會取得同步鎖的重入次數,如果是獲取了偏向鎖,那麼可能會多次獲取,state 的值會大於 1,這時候 c 的值 > 0 ,返回 false,解鎖失敗。如果 state = 1,那麼 c = 0,再判斷當前線程是否是獨佔鎖的線程,釋放獨佔鎖,返回 true,當 head 指向的頭結點不為 null,並且該節點的狀態值不為0的話才會執行 unparkSuccessor 方法,再進行鎖的獲取。

ReentrantLock源碼分析從入門到入土

ReentrantLock 其他方法

isHeldByCurrentThread & getHoldCount

在多線程同時訪問時,ReentrantLock 由最後一次成功鎖定的線程擁有,當這把鎖沒有被其他線程擁有時,線程調用 lock() 方法會立刻返回併成功獲取鎖。如果當前線程已經擁有鎖,這個方法會立刻返回。可以通過 isHeldByCurrentThreadgetHoldCount 來進行檢查。

首先來看 isHeldByCurrentThread 方法

public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}

根據方法名可以略知一二,是否被當前線程持有,它用來詢問鎖是否被其他線程擁有,這個方法和 Thread.holdsLock(Object) 方法內置的監視器鎖相同,而 Thread.holdsLock(Object) 是 Thread 類的靜態方法,是一個 native 類,它表示的意思是如果當前線程在某個對象上持有 monitor lock(監視器鎖) 就會返回 true。這個類沒有實際作用,僅僅用來測試和調試所用。例如

private ReentrantLock lock = new ReentrantLock();
public void lock(){
assert lock.isHeldByCurrentThread();
}

這個方法也可以確保重入鎖能夠表現出不可重入的行為

private ReentrantLock lock = new ReentrantLock();
public void lock(){
assert !lock.isHeldByCurrentThread();
lock.lock();
try {
// 執行業務代碼
}finally {
lock.unlock();
}
}

如果當前線程持有鎖則 lock.isHeldByCurrentThread() 返回 true,否則返回 false。

我們在瞭解它的用法後,看一下它內部是怎樣實現的,它內部只是調用了一下 sync.isHeldExclusively(),sync 是 ReentrantLock 的一個靜態內部類,基於 AQS 實現,而 AQS 它是一種抽象隊列同步器,是許多併發實現類的基礎,例如 ReentrantLock/Semaphore/CountDownLatch。sync.isHeldExclusively() 方法如下

protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}

此方法會在擁有鎖之前先去讀一下狀態,如果當前線程是鎖的擁有者,則不需要檢查。

getHoldCount()方法和isHeldByCurrentThread 都是用來檢查線程是否持有鎖的方法,不同之處在於 getHoldCount() 用來查詢當前線程持有鎖的數量,對於每個未通過解鎖操作匹配的鎖定操作,線程都會保持鎖定狀態,這個方法也通常用於調試和測試,例如

private ReentrantLock lock = new ReentrantLock();
public void lock(){
assert lock.getHoldCount() == 0;
lock.lock();
try {
// 執行業務代碼
}finally {
lock.unlock();
}
}

這個方法會返回當前線程持有鎖的次數,如果當前線程沒有持有鎖,則返回0。

newCondition 創建 ConditionObject 對象

ReentrantLock 可以通過 newCondition 方法創建 ConditionObject 對象,而 ConditionObject 實現了 Condition 接口,關於 Condition 的用法我們後面再講。

isLocked 判斷是否鎖定

查詢是否有任意線程已經獲取鎖,這個方法用來監視系統狀態,而不是用來同步控制,很簡單,直接判斷 state 是否等於0。

isFair 判斷是否是公平鎖的實例

這個方法也比較簡單,直接使用 instanceof 判斷是不是 FairSync 內部類的實例

public final boolean isFair() {
return sync instanceof FairSync;
}

getOwner 判斷鎖擁有者

判斷同步狀態是否為0,如果是0,則沒有線程擁有鎖,如果不是0,直接返回獲取鎖的線程。

final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}

hasQueuedThreads 是否有等待線程

判斷是否有線程正在等待獲取鎖,如果頭節點與尾節點不相等,說明有等待獲取鎖的線程。

public final boolean hasQueuedThreads() {
return head != tail;
}

isQueued 判斷線程是否排隊

判斷給定的線程是否正在排隊,如果正在排隊,返回 true。這個方法會遍歷隊列,如果找到匹配的線程,返回true

public final boolean isQueued(Thread thread) {
if (thread == null)
throw new NullPointerException();
for (Node p = tail; p != null; p = p.prev)
if (p.thread == thread)
return true;
return false;
}

getQueueLength 獲取隊列長度

此方法會返回一個隊列長度的估計值,該值只是一個估計值,因為在此方法遍歷內部數據結構時,線程數可能會動態變化。 此方法設計用於監視系統狀態,而不用於同步控制。

public final int getQueueLength() {
int n = 0;
for (Node p = tail; p != null; p = p.prev) {
if (p.thread != null)
++n;
}
return n;
}

getQueuedThreads 獲取排隊線程

返回一個包含可能正在等待獲取此鎖的線程的集合。 因為實際的線程集在構造此結果時可能會動態更改,所以返回的集合只是一個大概的列表集合。 返回的集合的元素沒有特定的順序。

public final Collection<Thread> getQueuedThreads() {
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node p = tail; p != null; p = p.prev) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
return list;
}

回答上面那個問題

那麼你看完源碼分析後,你能總結出 synchronizedlock 鎖的實現 ReentrantLock 有什麼異同嗎?

Synchronzied 和 Lock 的主要區別如下:

  • 存在層面:Syncronized 是Java 中的一個關鍵字,存在於 JVM 層面,Lock 是 Java 中的一個接口

  • 鎖的釋放條件:1. 獲取鎖的線程執行完同步代碼後,自動釋放;2. 線程發生異常時,JVM會讓線程釋放鎖;Lock 必須在 finally 關鍵字中釋放鎖,不然容易造成線程死鎖

  • 鎖的獲取: 在 Syncronized 中,假設線程 A 獲得鎖,B 線程等待。如果 A 發生阻塞,那麼 B 會一直等待。在 Lock 中,會分情況而定,Lock 中有嘗試獲取鎖的方法,如果嘗試獲取到鎖,則不用一直等待

  • 鎖的狀態:Synchronized 無法判斷鎖的狀態,Lock 則可以判斷

  • 鎖的類型:Synchronized 是可重入,不可中斷,非公平鎖;Lock 鎖則是 可重入,可判斷,可公平鎖

  • 鎖的性能:Synchronized 適用於少量同步的情況下,性能開銷比較大。Lock 鎖適用於大量同步階段:

    Lock 鎖可以提高多個線程進行讀的效率(使用 readWriteLock)

  • 在競爭不是很激烈的情況下,Synchronized的性能要優於ReetrantLock,但是在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態;

  • ReetrantLock 提供了多樣化的同步,比如有時間限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等

還有什麼要說的嗎

面試官可能還會問你 ReentrantLock 的加鎖流程是怎樣的,其實如果你能把源碼給他講出來的話,一定是高分。如果你記不住源碼流程的話可以記住下面這個簡化版的加鎖流程

  • 如果 lock 加鎖設置成功,設置當前線程為獨佔鎖的線程;

  • 如果 lock 加鎖設置失敗,還會再嘗試獲取一次鎖數量,

    如果鎖數量為0,再基於 CAS 嘗試將 state(鎖數量)從0設置為1一次,如果設置成功,設置當前線程為獨佔鎖的線程;

    如果鎖數量不為0或者上邊的嘗試又失敗了,查看當前線程是不是已經是獨佔鎖的線程了,如果是,則將當前的鎖數量+1;如果不是,則將該線程封裝在一個Node內,並加入到等待隊列中去。等待被其前一個線程節點喚醒。

ReentrantLock源碼分析從入門到入土
ReentrantLock源碼分析從入門到入土

文章參考:

【試驗局】ReentrantLock中非公平鎖與公平鎖的性能測試

第五章 ReentrantLock源碼解析1–獲得非公平鎖與公平鎖lock()

juejin.im/post/5c95df…

【JUC】JDK1.8源碼分析之ReentrantLock(三)

www.lagou.com/lgeduarticl…

相關文章

網頁設計中最常用的5種配圖

[前端]實戰年終盤點

一杯茶的時間,上手Docker

【canvas】箭頭跟隨鼠標移動的動畫原理