圖解ReentrantLock底層公平鎖和非公平鎖實現原理

image

💻在面試或者日常開發當中,經常會遇到公平鎖和非公平鎖的概念。

兩者最大的區別如下👇

1️⃣ 公平鎖:N個線程去申請鎖時,會按照先後順序進入一個隊列當中去排隊,依次按照先後順序獲取鎖。就像下圖描述的上廁所的場景一樣,先來的先佔用廁所,後來的只能老老實實排隊。

image

2️⃣ 非公平鎖:N個線程去申請鎖,會直接去競爭鎖,若能獲取鎖就直接佔有,獲取不到鎖,再進入隊列排隊順序等待獲取鎖。同樣以排隊上廁所打比分,這時候,後來的線程會先嘗試插隊看看能否搶佔到廁所,若能插隊搶佔成功,就能使用廁所,若失敗就得老老實實去隊伍後面排隊。
image

針對這兩個概念,我們通過ReentrantLock底層源碼來分析下💁 :公平鎖和非公平鎖在ReentrantLock類當中鎖怎樣實現的。

🌈ReentrantLock內部實現的公平鎖類是FairSync,非公平鎖類是NonfairSync。

當ReentrantLock以無參構造器創建對象時,默認生成的是非公平鎖對象NonfairSync,只有帶參且參數為true的情況下FairSync,才會生成公平鎖,若傳參為false時,生成的依然是非公平鎖,兩者構造器源碼結果如下👇
image

​ 圖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方法里。

image

可以看到,在非公平鎖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,說明已有線程佔有鎖資源,其他線程需要等待該佔有鎖的線程釋放鎖資源後,方能進行搶佔鎖的動作。
image

線程在搶佔鎖時,是通過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方法。

image

NonfairSync類的acquire方法的流程圖如下👇

image

先分析非公平鎖的!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區別👇

image

可以看到,公平鎖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節點,就只能老老實實去後面排隊等待獲取鎖了。

image