圖解ReentrantLock底層公平鎖和非公平鎖實現原理
💻在面試或者日常開發當中,經常會遇到公平鎖和非公平鎖的概念。
兩者最大的區別如下👇
1️⃣ 公平鎖:N個線程去申請鎖時,會按照先後順序進入一個隊列當中去排隊,依次按照先後順序獲取鎖。就像下圖描述的上廁所的場景一樣,先來的先佔用廁所,後來的只能老老實實排隊。
2️⃣ 非公平鎖:N個線程去申請鎖,會直接去競爭鎖,若能獲取鎖就直接佔有,獲取不到鎖,再進入隊列排隊順序等待獲取鎖。同樣以排隊上廁所打比分,這時候,後來的線程會先嘗試插隊看看能否搶佔到廁所,若能插隊搶佔成功,就能使用廁所,若失敗就得老老實實去隊伍後面排隊。
針對這兩個概念,我們通過ReentrantLock底層源碼來分析下💁 :公平鎖和非公平鎖在ReentrantLock類當中鎖怎樣實現的。
🌈ReentrantLock內部實現的公平鎖類是FairSync,非公平鎖類是NonfairSync。
當ReentrantLock以無參構造器創建對象時,默認生成的是非公平鎖對象NonfairSync,只有帶參且參數為true的情況下FairSync,才會生成公平鎖,若傳參為false時,生成的依然是非公平鎖,兩者構造器源碼結果如下👇
圖1
在實際開發當中,關於ReentrantLock的使用案例,一般是這個格式👇
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock();
// block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
這時的lock指向的其實是NonfairSync對象,即非公平鎖。
當使用lock.lock()對臨界區進行占鎖操作時,最終會調用到NonfairSync對象的lock()方法。根據圖1可知,NonfairSync和FairSync兩者的lock方法實現邏輯是不一樣的,而體現其鎖是否符合公平與否的地方,就是在兩者的lock方法里。
可以看到,在非公平鎖NonfairSync的上鎖lock方法當中,若if(compareAndSetState(0,1))判斷不滿足,就會執行acquire(1)方法,該方法跟公平鎖FairSync的lock方法里調用的acquire(1)其實是同一個,但方法里的tryAcquire具體實現又存在略微不同,這裡後面會討論。
這裡就呼應前文提到的非公平鎖的概念——當N個線程去申請非公平鎖,它們會直接去競爭鎖,若能獲取鎖就直接佔有,獲取不到鎖,再進入隊列排隊順序等待獲取鎖。這裡的「獲取不到鎖,再進入隊列排隊順序等待獲取鎖」可以理解成⏩——當線程過來直接競爭鎖失敗後,就會變成公平鎖的形式,進入到一個隊列當中,按照先後順序排隊去獲取鎖。
而if(compareAndSetState(0,1))語句塊的邏輯,恰好就體現了「當N個線程去申請非公平鎖,它們會直接去競爭鎖,若能獲取鎖就直接佔有」這句話的意思。
🌈首先,先來分析NonfairSync的lock()方法原理,源碼如下👇
final void lock() {
//先競爭鎖,若能競爭成功,則佔有鎖資源
if (compareAndSetState(0, 1))
//將獨佔線程成員變量設置為當前線程,表示佔有鎖資源的線程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
compareAndSetState(0, 1)就是一個當前線程與其他線程搶佔鎖的過程,這裏面涉及到AQS的知識點,因此,閱讀本文時,需具備一定的AQS基礎。
JUC的鎖實現是基於AQS實現的,可以簡單理解成,AQS里定義了一個private volatile int state變量,若state值為0,說明無線程佔有,其他線程可以進行搶佔鎖資源;若state值為1,說明已有線程佔有鎖資源,其他線程需要等待該佔有鎖的線程釋放鎖資源後,方能進行搶佔鎖的動作。
線程在搶佔鎖時,是通過CAS對state變量進行置換操作的,期望值expect是0,更新值update為1,若期望值expect能與內存地址里的state值一致,就可以通過原子操作將內存地址里state值置換成更新值update,返回true,反之,就置換失敗返回false。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
可見,這裡的if (compareAndSetState(0, 1))就體現了非公平鎖的機制,當前線程會先去競爭鎖,若能競爭成功,就佔有鎖資源。
若競爭鎖失敗話,就會執行acquire(1)方法,其原理就相當走跟公平鎖類似的邏輯。
acquire(1);
進入acquire方法,該方法是位於AbstractQueuedSynchronizer里,就是前文提到的AQS,即抽象同步隊列器,它相當提供一套用戶態層面的鎖框架,基於它可以實現用戶態層面的鎖機制。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
注意一點,NonfairSync和FairSync調用的acquire(int arg)方法中的tryAcquire方法,其實現是不同的。NonfairSync調用的acquire方法,其底層tryAcquire調用的是NonfairSync重寫的tryAcquire方法;FairSync調用的acquire方法,其底層tryAcquire調用的是FairSync重寫的tryAcquire方法。
NonfairSync類的acquire方法的流程圖如下👇
先分析非公平鎖的!tryAcquire(arg)底層源碼實現,該方法的整體邏輯是,通過getState()獲取state狀態值,判斷是否已為0。若state等於0了,說明此時鎖資源處於無鎖狀態,那麼,當前線程就可以直接再執行一遍CAS原子搶鎖操作,若CAS成功,說明已成功搶佔鎖。若state不為0,再判斷當前線程是否與佔有資源的鎖為同一個線程,若同一個線程,那麼就進行重入鎖操作,即ReentrantLock支持同一個線程對資源的重複加鎖,每次加鎖,就對state值加1,解鎖時,就對state解鎖,直至減到0最後釋放鎖。
🌈最後,若在該方法里,通過CAS搶佔鎖成功或者重入鎖成功,那麼就會返回true,若失敗,就會返回false。
final boolean nonfairTryAcquire(int acquires) {
//獲取當前線程引用
final Thread current = Thread.currentThread();
//獲取AQS的state狀態值
int c = getState();
//若state等於0了,說明鎖處於無被佔用狀態,可被當前線程搶佔
if (c == 0) {
//再次嘗試通過CAS搶鎖
if (compareAndSetState(0, acquires)) {
//將獨佔線程成員變量設置為當前線程,表示佔有鎖資源的線程
setExclusiveOwnerThread(current);
return true;
}
}
//判斷當前線程是否與佔有鎖資源的線程為同一個線程
else if (current == getExclusiveOwnerThread()) {
//每次重入鎖,state就會加1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
在 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))代碼當中,根據 &&短路機制,若!tryAcquire(arg)為false,就不會再執行後面代碼。反之,若!tryAcquire(arg)為true,說明搶佔鎖失敗了或者不屬於重入鎖,那麼就會繼續後續acquireQueued(addWaiter(Node.EXCLUSIVE), arg))代碼的執行。acquireQueued裏面的邏輯,就可以理解成「獲取不到鎖,再進入隊列排隊順序等待獲取鎖」。這塊內容涉及比較複雜的雙向鏈表邏輯,我後面會另外寫一篇文章深入分析,本文主要是講解公平鎖和非公平鎖的區別科普。
FairSync公平鎖lock方法里acquire(1)的邏輯與非公平鎖NonfairSync的acquire(1)很類似,其底層實現同樣是這樣👇
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我們來看下FairSync類里重實現的tryAcquire與NonfairSync最終執行的tryAcquire區別👇
可以看到,公平鎖FairSync的tryAcquire方法比NonfairSync的nonfairTryAcquire方法多了一行!hasQueuedPredecessors()代碼。
在FairSync公平鎖里,若hasQueuedPredecessors()返回false,那麼!hasQueuedPredecessors()就會為true,在執行以下判斷時,就會通過compareAndSetState(0, acquires)即CAS原子搶佔鎖。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
那麼,什麼情況下,hasQueuedPredecessors() 能得到false值呢?
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
❗存在兩種情況:
1️⃣ 第一種情況,h != t為false,說明head和tail節點都為null或者h和t都指向一個假節點head,這兩種情況都說明了,此時的同步隊列還沒有初始化,簡單點理解,就是在當前線程之前,還沒有出現線程去搶佔鎖,因此,此時,鎖是空閑的, 同時當前線程算上最早到來的線程之一(高並發場景下同一時刻可能存在N個線程同時到來),就可以通過CAS競爭鎖。
2️⃣ 第二種情況,h != t為true但(s = h.next) == null || s.thread != Thread.currentThread()為false,當頭節點head和尾節點都不為空且指向不是同一節點,就說明同步隊列已經初始化,此時至少存在兩個以上節點,那麼head.next節點必定不為空,即(s = h.next) == null會為false,若s.thread != Thread.currentThread()為false,說明假節點head的next節點剛好與當前線程為同一節點,也就意味着,當前線程排在隊列最前面,排在前面的可以在鎖空閑時獲取鎖資源,就可以執行compareAndSetState(0, acquires)去搶佔鎖資源。
若同步隊列已經初始化,且當前線程又不是在假節點head的next節點,就只能老老實實去後面排隊等待獲取鎖了。