這篇 ReentrantLock 看不懂,加我我給你發紅包

加個“星標”,一起快樂成長

回答一個問題

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

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

初識 ReentrantLock

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

ReentrantLock 基本方法

構造方法

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

<code>public ReentrantLock {
sync = new NonfairSync;
}

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

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

<code>// 創建的是公平鎖
private ReentrantLock lock = new ReentrantLock(true);

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

// 默認創建非公平鎖
private ReentrantLock lock = new ReentrantLock;
/<code>

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

这篇 ReentrantLock 看不懂,加我我给你发红包
<code>abstract static class Sync extends AbstractQueuedSynchronizer {...}

static final class FairSync extends Sync {...}

static final class NonfairSync extends Sync {...}
/<code>

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

下面是一個<code>公平鎖/<code>的實現

<code>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;
}
}
}

/<code>

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

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

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

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

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

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

<code>if(currentValue == expectedValue){
currentValue = updateValue
}
/<code>

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

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

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

然後看一下第二條路線 addWaiter

这篇 ReentrantLock 看不懂,加我我给你发红包

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

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

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

在看第三條路線 acquireQueued

这篇 ReentrantLock 看不懂,加我我给你发红包

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

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

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

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

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

如果 <code>!tryAcquire/<code>和<code>acquireQueued/<code>都為 true 的話,則打斷當前線程。

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

这篇 ReentrantLock 看不懂,加我我给你发红包

lockInterruptibly 以可中斷的方式獲取鎖

以下是 JavaDoc 官方解釋:

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

  • 鎖被當前線程持有

  • 一些其他線程打斷了當前線程

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

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

  • 在進入此方法時設置了其中斷狀態

  • 當獲取鎖的時候發生了中斷(Thread.interrupt)

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

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

下面是它的流程圖

这篇 ReentrantLock 看不懂,加我我给你发红包

tryLock 嘗試加鎖

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

这篇 ReentrantLock 看不懂,加我我给你发红包

tryLock 超時獲取鎖

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

<code>if (lock.tryLock || lock.tryLock(timeout, unit)) {...}
/<code>

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

它的源碼如下

这篇 ReentrantLock 看不懂,加我我给你发红包

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

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

这篇 ReentrantLock 看不懂,加我我给你发红包

unlock 解鎖流程

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

这篇 ReentrantLock 看不懂,加我我给你发红包

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

这篇 ReentrantLock 看不懂,加我我给你发红包

ReentrantLock 其他方法

isHeldByCurrentThread & getHoldCount

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

首先來看 isHeldByCurrentThread 方法

<code>public boolean isHeldByCurrentThread {
return sync.isHeldExclusively;
}
/<code>

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

<code>private ReentrantLock lock = new ReentrantLock;

public void lock{
assert lock.isHeldByCurrentThread;
}
/<code>

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

<code>private ReentrantLock lock = new ReentrantLock;

public void lock{
assert !lock.isHeldByCurrentThread;
lock.lock;

try {
// 執行業務代碼
}finally {
lock.unlock;
}
}
/<code>

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

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

<code>protected final boolean isHeldExclusively {
return getExclusiveOwnerThread == Thread.currentThread;
}
/<code>

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

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

<code>private ReentrantLock lock = new ReentrantLock;

public void lock{
assert lock.getHoldCount == 0;

lock.lock;
try {
// 執行業務代碼
}finally {
lock.unlock;
}
}
/<code>

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

newCondition 創建 ConditionObject 對象

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

isLocked 判斷是否鎖定

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

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

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

<code>public final boolean isFair {
return sync instanceof FairSync;
}
/<code>

getOwner 判斷鎖擁有者

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

<code>final Thread getOwner {
return getState == 0 ? : getExclusiveOwnerThread;
}
/<code>

hasQueuedThreads 是否有等待線程

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

<code>public final boolean hasQueuedThreads {
return head != tail;
}
/<code>

isQueued 判斷線程是否排隊

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

<code>public final boolean isQueued(Thread thread) {
if (thread == )
throw new PointerException;
for (Node p = tail; p != ; p = p.prev)
if (p.thread == thread)
return true;
return false;
}
/<code>

getQueueLength 獲取隊列長度

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

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

getQueuedThreads 獲取排隊線程

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

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

回答上面那個問題

那麼你看完源碼分析後,你能總結出 <code>synchronized/<code>和<code>lock/<code>鎖的實現<code>ReentrantLock/<code>有什麼異同嗎?

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源碼解析1--獲得非公平鎖與公平鎖lock

https://juejin.im/post/5c95df97e51d4551d06d8e8e

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

https://

www.lagou.com/lgeduarticle/73019.html

END


公眾號(zhisheng)裡回覆 面經、ES、Flink、 Spring、Java、Kafka、監控 等關鍵字可以查看更多關鍵字對應的文章。


分享到:


相關文章: