從原始碼入手詳解ReentrantLock,一個比synchronized更強大的可重入鎖

JavaBuild發表於2024-04-21

寫在開頭

隨手一翻,發現對於Java中併發多執行緒的學習已經發布了十幾篇部落格了,多執行緒 是Java基礎中的重中之重!因此,可能還需要十幾篇部落格才能大致的講完這部分的知識點,初學者對於這部分內容一定要多花心思,不可馬虎!今天我們繼續來學習一個重要知識點:ReentrantLock

ReentrantLock :是一種獨佔式的可重入鎖,位於java.util.concurrent.locks中,是Lock介面的預設實現類,底部的同步特性基於AQS實現,和synchronized關鍵字類似,但更靈活、功能更強大、也是目前實戰中使用頻率非常高的同步類。

幾種不同鎖的定義

在學習ReentrantLock之前,我們先來複習一下如下的幾類鎖的定義,這個其實很早的博文中就已經詳細的整理過了,這裡為了更好理解ReentrantLock鎖,還是簡單羅列一下。

獨佔鎖與共享鎖

  1. 獨佔鎖:同一時間,一把鎖只能被一個執行緒獲取;
  2. 共享鎖:同意時間,一把鎖可以被多個執行緒獲取。

公平鎖與非公平鎖

  1. 公平鎖:按照申請鎖的時間先後,進行鎖的再分配工作,這種鎖往往效能稍差,因為要保證申請時間上的順序性;
  2. 非公平鎖: 鎖被釋放後,後續執行緒獲得鎖的可能性隨機,或者按照設定的優先順序進行搶佔式獲取鎖。

可重入鎖

所謂可重入鎖就是一個執行緒在獲取到了一個物件鎖後,執行緒內部再次獲取該鎖,依舊可以獲得,即便持有的鎖還沒釋放,仍然可以獲得,不可重入鎖這種情況下會發生死鎖!

可重入鎖在使用時需要注意的是:由於鎖會被獲取 n 次,那麼只有鎖在被釋放同樣的 n 次之後,該鎖才算是完全釋放成功。

可中斷鎖與不可中斷鎖

  1. 可中斷鎖:在獲取鎖的過程中可以中斷獲取,不需要非得等到獲取鎖後再去執行其他邏輯;
  2. 不可中斷鎖:一旦執行緒申請了鎖,就必須等待獲取鎖後方能執行其他的邏輯處理。

ReentrantLock是一種同時擁有獨佔式、可重入、可中斷、公平/非公平特性的同步器!


ReentrantLock

根據上面總結出的特點,我們從底層原始碼出發來驗證一下結論的準確性,首先我們透過一個關係圖譜來大致梳理一下ReentrantLock的內部構造。

image

ReentrantLock實現了Lock和Serializable介面:

public class ReentrantLock implements Lock, java.io.Serializable {}

其內部擁有三個內部類,分別為Sync、FairSync、NonfariSync,其中FairSync、NonfariSync繼承父類Sync。Sync又繼承了AQS(AbstractQueuedSynchronizer),新增鎖和釋放鎖的大部分操作實際上都是在 Sync 中實現的。

問題1:ReentrantLock內部公平鎖與非公平鎖如何實現?

在內部透過構造器來實現公平鎖與非公平鎖的設定,預設為非公平鎖,同樣可以透過傳參設定為公平鎖。底層實現其實是透過FairSync、NonfariSync這個兩個內部類,原始碼如下:

//無參構造,預設為非公平鎖
public ReentrantLock() {
    sync = new NonfairSync();
}
// 傳入一個 boolean 值,true 時為公平鎖,false 時為非公平鎖
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

問題2:獨佔鎖如何實現?

在原始碼中無論是Sync這個內部類或是其子類,都會呼叫setExclusiveOwnerThread(current)這個方法,這個方法是AQS的父類AOS(AbstractOwnableSynchronizer)中的方法,用以標記鎖的持有者為獨佔模式。

image

問題3:ReentrantLock如何獲取和釋放鎖?

由於ReentrantLock是預設非公平鎖,所以我們就以非公平模式為例去看一下它底層如何實現鎖的獲取與釋放的。

1️⃣ 鎖的獲取

核心方法為Sync內部類的nonfairTryAcquire方法,如下為其原始碼,先獲取當前鎖的狀態,若為0說明沒有被任何執行緒獲取,此時直接獲取即可;另外一種state不為0時,則需要判斷佔有執行緒是否為當前執行緒,若是則可以獲取,並將state值加一返回,否則獲取失敗。

【注意】:公平模式下獲取鎖的時會多一步呼叫hasQueuedPredecessors 的邏輯判斷,用以判斷當前執行緒對應的節點在等待佇列中是否有前驅節點,畢竟公平鎖的競爭嚴格按照獲取鎖的時間進行分配的。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //1. 如果該鎖未被任何執行緒佔有,該鎖能被當前執行緒獲取
	if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
	//2.若被佔有,檢查佔有執行緒是否是當前執行緒
    else if (current == getExclusiveOwnerThread()) {
		// 3. 再次獲取,計數加一
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

2️⃣ 鎖的釋放

對應的以非公平鎖中釋放為例,透過原始碼我們可以看到,每呼叫一次則同步狀態減1,直至同步狀態為0,鎖才被完全的釋放完,否則返回false。

protected final boolean tryRelease(int releases) {
	//1. 同步狀態減1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
		//2. 只有當同步狀態為0時,鎖成功被釋放,返回true
        free = true;
        setExclusiveOwnerThread(null);
    }
	// 3. 鎖未被完全釋放,返回false
    setState(c);
    return free;
}

3️⃣ 小總結

經過上面原始碼的學習,我們已經能夠確認一點就是:ReentrantLock是一種同時擁有獨佔式、可重入、可中斷、公平/非公平特性的同步器!我們接下來就繼續再來學習一下它的使用。

問題4:ReentrantLock的使用

我們透過一個小demo,來感受一下基於非公平鎖模式下的ReentrantLock的使用哈

public class Test {
    //初始化一個靜態lock物件
    private static final ReentrantLock lock = new ReentrantLock();
    //初始化計算量值
    private static int count;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("result:"+count);
    }
}

上面這個程式預期輸出結果為:2000,thread1和thread2分別做了加1000次的操作,由於ReentrantLock是獨佔式可重入鎖,故最終可以成功列印出預期結果!

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

image

相關文章