Java 重入鎖 ReentrantLock 原理分析

quxing10086發表於2018-05-08

1.簡介

可重入鎖ReentrantLock自 JDK 1.5 被引入,功能上與synchronized關鍵字類似。所謂的可重入是指,執行緒可對同一把鎖進行重複加鎖,而不會被阻塞住,這樣可避免死鎖的產生。ReentrantLock 的主要功能和 synchronized 關鍵字一致,均是用於多執行緒的同步。但除此之外,ReentrantLock 在功能上比 synchronized 更為豐富。比如 ReentrantLock 在加鎖期間,可響應中斷,可設定超時等。

ReentrantLock 是我們日常使用很頻繁的一種鎖,所以在使用之餘,我們也應該去了解一下它的內部實現原理。ReentrantLock 內部是基於 AbstractQueuedSynchronizer(以下簡稱AQS)實現的。所以要想理解 ReentrantLock,應先去 AQS 相關原理。我在之前的文章 AbstractQueuedSynchronizer 原理分析 - 獨佔/共享模式 中,已經詳細分析過 AQS 原理,有興趣的朋友可以去看看。本文僅會在需要的時候對 AQS 相關原理進行簡要說明,更詳細的說明請參考我的其他文章。

2.原理

本章將會簡單介紹重入鎖 ReentrantLock 中的一些概念和相關原理,包括可重入、公平和非公平鎖等原理。在介紹這些原理前,首先我會介紹 ReentrantLock 與 synchronized 關鍵字的相同和不同之處。在此之後才回去介紹重入、公平和非公平等原理。

2.1 與 synchronized 的異同

ReentrantLock 和 synchronized 都是用於執行緒的同步控制,但它們在功能上來說差別還是很大的。對比下來 ReentrantLock 功能明顯要豐富的多。下面簡單列舉一下兩者之間的差異,如下:

特性synchronizedReentrantLock相同
可重入
響應中斷
超時等待
公平鎖
非公平鎖
是否可嘗試加鎖
是否是Java內建特性
自動獲取/釋放鎖
對異常的處理自動釋放鎖需手動釋放鎖

除此之外,ReentrantLock 提供了豐富的介面用於獲取鎖的狀態,比如可以通過isLocked()查詢 ReentrantLock 物件是否處於鎖定狀態, 也可以通過getHoldCount()獲取 ReentrantLock 的加鎖次數,也就是重入次數等。而 synchronized 僅支援通過Thread.holdsLock查詢當前執行緒是否持有鎖。另外,synchronized 使用的是物件或類進行加鎖,而 ReentrantLock 內部是通過 AQS 中的同步佇列進行加鎖,這一點和 synchronized 也是不一樣的。

這裡列舉了不少兩者的相同和不同之處,暫時這能想到這些。如果還有其他的區別,歡迎補充。

2.2 可重入

可重入這個概念並不難理解,本節通過一個例子簡單說明一下。

現在有方法 m1 和 m2,兩個方法均使用了同一把鎖對方法進行同步控制,同時方法 m1 會呼叫 m2。執行緒 t 進入方法 m1 成功獲得了鎖,此時執行緒 t 要在沒有釋放鎖的情況下,呼叫 m2 方法。由於 m1 和 m2 使用的是同一把可重入鎖,所以執行緒 t 可以進入方法 m2,並再次獲得鎖,而不會被阻塞住。示例程式碼大致如下:

void m1() {
    lock.lock();
    try {
        // 呼叫 m2,因為可重入,所以並不會被阻塞
        m2();
    } finally {
        lock.unlock()
    }
}

void m2() {
    lock.lock();
    try {
        // do something
    } finally {
        lock.unlock()
    }
}

假如 lock 是不可重入鎖,那麼上面的示例程式碼必然會引起死鎖情況的發生。這裡請大家思考一個問題,ReentrantLock 的可重入特性是怎樣實現的呢?簡單說一下,ReentrantLock 內部是通過 AQS 實現同步控制的,AQS 有一個變數 state 用於記錄同步狀態。初始情況下,state = 0,表示 ReentrantLock 目前處於解鎖狀態。如果有執行緒呼叫 lock 方法進行加鎖,state 就由0變為1,如果該執行緒再次呼叫 lock 方法加鎖,就讓其自增,即 state++。執行緒每呼叫一次 unlock 方法釋放鎖,會讓 state--。通過查詢 state 的數值,即可知道 ReentrantLock 被重入的次數了。這就是可重複特性的大致實現流程。

2.3 公平與非公平

公平與非公平指的是執行緒獲取鎖的方式。公平模式下,執行緒在同步佇列中通過 FIFO 的方式獲取鎖,每個執行緒最終都能獲取鎖。在非公平模式下,執行緒會通過“插隊”的方式去搶佔鎖,搶不到的則進入同步佇列進行排隊。預設情況下,ReentrantLock 使用的是非公平模式獲取鎖,而不是公平模式。不過我們也可通過 ReentrantLock 構造方法ReentrantLock(boolean fair)調整加鎖的模式。

既然既然有兩種不同的加鎖模式,那麼他們有什麼優缺點呢?答案如下:

公平模式下,可保證每個執行緒最終都能獲得鎖,但效率相對比較較低。非公平模式下,效率比較高,但可能會導致執行緒出現飢餓的情況。即一些執行緒遲遲得不到鎖,每次即將到手的鎖都有可能被其他執行緒搶了。這裡再提個問題,為啥非公平模式搶了其他執行緒獲取鎖的機會,而整個程式的執行效率會更高呢?說實話,開始我也不明白。不過好在《Java併發程式設計實戰》第13.3節 公平性(p232)說明了具體的原因,這裡引用一下:

在激烈競爭的情況下,非公平鎖的效能高於公平鎖的效能的一個原因是:在恢復一個被掛起的執行緒與該執行緒真正開始執行之間存在著嚴重的延遲。假設執行緒 A 持有一個鎖,並且執行緒 B 請求這個鎖。由於這個執行緒已經被執行緒 A 持有,因此 B 將被掛起。當 A 釋放鎖時,B 將被喚醒,因此會再次嘗試獲取鎖。與此同時,如果 C 也請求這個鎖,那麼 C 很有可能會在 B 被完全喚醒前獲得、使用以及釋放這個鎖。這樣的情況時一種“雙贏”的局面:B 獲得鎖的時刻並沒有推遲,C 更早的獲得了鎖,並且吞吐量也獲得了提高。

上面的原因大家看懂了嗎?下面配個圖輔助說明一下:

如上圖,執行緒 C 線上程 B 甦醒階段內獲取和使用鎖,並線上程 B 獲取鎖前釋放了鎖,所以執行緒 B 可以順利獲得鎖。執行緒 C 在搶佔鎖的情況下,仍未影響執行緒 B 獲取鎖,因此是個“雙贏”的局面。

除了上面的原因外,《Java併發程式設計的藝術》在其5.3.2 公平與非公平鎖的區別(p137)分析了另一個可能的原因。即公平鎖執行緒切換次數要比非公平鎖執行緒切換次數多得多,因此效率上要低一些。更多的細節,可以參考作者的論述,這裡不展開說明了。

本節最後說一下公平鎖和非公平鎖的使用場景。如果執行緒持鎖時間短,則應使用非公平鎖,可通過“插隊”提升效率。如果執行緒持鎖時間長,“插隊”帶來的效率提升可能會比較小,此時應使用公平鎖。

3. 原始碼分析

3.1 程式碼結構

前面說到 ReentrantLock 是基於 AQS 實現的,AQS 很好的封裝了同步佇列的管理,執行緒的阻塞與喚醒等基礎操作。基於 AQS 的同步元件,推薦的使用方式是通過內部非 public 靜態類繼承 AQS,並重寫部分抽象方法。其程式碼結構大致如下:

15256891997562

上圖中,Sync是一個靜態抽象類,繼承了 AbstractQueuedSynchronizer。公平和非公平鎖的實現類NonfairSyncFairSync則繼承自 Sync 。至於 ReentrantLock 中的其他一些方法,主要邏輯基本上都在幾個內部類中實現的。

3.2 獲取鎖

在分析 ReentrantLock 加鎖的程式碼前,下來簡單介紹一下 AQS 同步佇列的一些知識。AQS 維護了一個基於雙向連結串列的同步佇列,執行緒在獲取同步狀態失敗的情況下,都會被封裝成節點,然後加入佇列中。同步佇列大致示意圖如下:

在同步佇列中,頭結點是獲取同步狀態的節點。其他節點在嘗試獲取同步狀態失敗後,會被阻塞住,暫停執行。當頭結點釋放同步狀態後,會喚醒其後繼節點。後繼節點會將自己設為頭節點,並將原頭節點從佇列中移除。大致示意圖如下:

介紹完 AQS 同步佇列,以及節點執行緒獲取同步狀態的過程。下面來分析一下 ReentrantLock 中獲取鎖方法的原始碼,如下:

public void lock() {
    sync.lock();
}

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 這裡的 lock 是抽象方法,具體的實現在兩個子類中
    abstract void lock();
    
    // 省略其他無關程式碼
}

lock 方法的實現很簡單,不過這裡的 lock 方法只是一個殼子而已。由於獲取鎖的方式有公平和非公平之分,所以具體的實現是在NonfairSyncFairSync兩個類中。那麼我們繼續往下分析一下這兩個類的實現。

3.2.1 公平鎖

公平鎖對應的邏輯是 ReentrantLock 內部靜態類 FairSync,我們沿著上面的 lock 方法往下分析,如下:

+--- ReentrantLock.FairSync.java
final void lock() {
    // 呼叫 AQS acquire 獲取鎖
    acquire(1);
}

+--- AbstractQueuedSynchronizer.java
/**
 * 該方法主要做了三件事情:
 * 1. 呼叫 tryAcquire 嘗試獲取鎖,該方法需由 AQS 的繼承類實現,獲取成功直接返回
 * 2. 若 tryAcquire 返回 false,則呼叫 addWaiter 方法,將當前執行緒封裝成節點,
 *    並將節點放入同步佇列尾部
 * 3. 呼叫 acquireQueued 方法讓同步佇列中的節點迴圈嘗試獲取鎖
 */
public final void acquire(int arg) {
    // acquireQueued 和 addWaiter 屬於 AQS 中的方法,這裡不展開分析了
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

+--- ReentrantLock.FairSync.java
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 獲取同步狀態
    int c = getState();
    // 如果同步狀態 c 為0,表示鎖暫時沒被其他執行緒獲取
    if (c == 0) {
        /*
         * 判斷是否有其他執行緒等待的時間更長。如果有,應該先讓等待時間更長的節點先獲取鎖。
         * 如果沒有,呼叫 compareAndSetState 嘗試設定同步狀態。
         */ 
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 將當前執行緒設定為持有鎖的執行緒
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果當前執行緒為持有鎖的執行緒,則執行重入邏輯
    else if (current == getExclusiveOwnerThread()) {
        // 計算重入後的同步狀態,acquires 一般為1
        int nextc = c + acquires;
        // 如果重入次數超過限制,這裡會丟擲異常
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 設定重入後的同步狀態
        setState(nextc);
        return true;
    }
    return false;
}

+--- AbstractQueuedSynchronizer.java
/** 該方法用於判斷同步佇列中有比當前執行緒等待時間更長的執行緒 */
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    /*
     * 在同步佇列中,頭結點是已經獲取了鎖的節點,頭結點的後繼節點則是即將獲取鎖的節點。
     * 如果有節點對應的執行緒等待的時間比當前執行緒長,則返回 true,否則返回 false
     */
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

ReentrantLock 中獲取鎖的流程並不是很複雜,上面的程式碼執行流程如下:

  1. 呼叫 acquire 方法,將執行緒放入同步佇列中進行等待
  2. 執行緒在同步佇列中成功獲取鎖,則將自己設為持鎖執行緒後返回
  3. 若同步狀態不為0,且當前執行緒為持鎖執行緒,則執行重入邏輯

3.2.2 非公平鎖

分析完公平鎖相關程式碼,下面再來看看非公平鎖的原始碼分析,如下:

+--- ReentrantLock.NonfairSync
final void lock() {
    /*
     * 這裡呼叫直接 CAS 設定 state 變數,如果設定成功,表明加鎖成功。這裡並沒有像公平鎖
     * 那樣呼叫 acquire 方法讓執行緒進入同步佇列進行排隊,而是直接呼叫 CAS 搶佔鎖。搶佔失敗
     * 再呼叫 acquire 方法將執行緒置於佇列尾部排隊。
     */
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

+--- AbstractQueuedSynchronizer
/** 參考上一節的分析 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

+--- ReentrantLock.NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

+--- ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 獲取同步狀態
    int c = getState();
    
    // 如果同步狀態 c = 0,表明鎖當前沒有執行緒獲得,此時可加鎖。
    if (c == 0) {
        // 呼叫 CAS 加鎖,如果失敗,則說明有其他執行緒在競爭獲取鎖
        if (compareAndSetState(0, acquires)) {
            // 設定當前執行緒為鎖的持有執行緒
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果當前執行緒已經持有鎖,此處條件為 true,表明執行緒需再次獲取鎖,也就是重入
    else if (current == getExclusiveOwnerThread()) {
        // 計算重入後的同步狀態值,acquires 一般為1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 設定新的同步狀態值
        setState(nextc);
        return true;
    }
    return false;
}

非公平鎖的實現也不是很複雜,其加鎖的步驟大致如下:

  1. 呼叫 compareAndSetState 方法搶佔式加鎖,加鎖成功則將自己設為持鎖執行緒,並返回
  2. 若加鎖失敗,則呼叫 acquire 方法,將執行緒置於同步佇列尾部進行等待
  3. 執行緒在同步佇列中成功獲取鎖,則將自己設為持鎖執行緒後返回
  4. 若同步狀態不為0,且當前執行緒為持鎖執行緒,則執行重入邏輯

3.2.3 公平和非公平細節對比

如果大家之前閱讀過公平鎖和非公平鎖的原始碼,會發現兩者之間的差別不是很大。為了找出它們之間的差異,這裡我將兩者的對比程式碼放在一起,大家可以比較一下,如下:

從上面的原始碼對比圖中,可以看出兩種的差異並不大。那麼現在請大家思考一個問題:在程式碼差異不大情況下,是什麼差異導致了公平鎖和非公平鎖的產生呢?大家先思考一下,答案將會在下面展開說明。

在上面的原始碼對比圖中,左邊是非公平鎖的實現,右邊是公平鎖的實現。從對比圖中可看出,兩者的 lock 方法有明顯區別。非公平鎖的 lock 方法會首先嚐試去搶佔設定同步狀態,而不是直接呼叫 acquire 將執行緒放入同步佇列中等待獲取鎖。除此之外,tryAcquire 方法實現上也有差異。由於非公平鎖的 tryAcquire 邏輯主要封裝在 Sync 中的 nonfairTryAcquire 方法裡,所以我們直接對比這個方法即可。由上圖可以看出,Sync 中的 nonfairTryAcquire 與公平鎖中的 tryAcquire 實現上差異並不大,唯一的差異在第18行,這裡我用一條紅線標註了出來。公平鎖的 tryAcquire 在第18行多出了一個條件,即!hasQueuedPredecessors()。這個方法的目的是判斷是否有其他執行緒比當前執行緒在同步佇列中等待的時間更長。有的話,返回 true,否則返回 false。比如下圖:

node1 對應的執行緒比 node2 對應的執行緒在佇列中等待的時間更長,如果 node2 執行緒呼叫 hasQueuedPredecessors 方法,則會返回 true。如果 node1 呼叫此方法,則會返回 false。因為 node1 前面只有一個頭結點,但頭結點已經獲取同步狀態,不處於等待狀態。所以在所有處於等待狀態的節點中,沒有節點比它等待的更長了。理解了 hasQueuedPredecessors 方法的用途後,那麼現在請大家思考個問題,假如把條件去掉對公平鎖會有什麼影響呢?答案在 lock 所呼叫的 acquire 方法中,再來看一遍 acquire 方法原始碼:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire 方法先呼叫子類實現的 tryAcquire 方法,用於嘗試獲取同步狀態,呼叫成功則直接返回。若呼叫失敗,則應將執行緒插入到同步佇列尾部,按照 FIFO 原則獲取鎖。如果我們把 tryAcquire 中的條件!hasQueuedPredecessors()去掉,公平鎖將不再那麼“謙讓”,它將會像非公平鎖那樣搶佔獲取鎖,搶佔失敗才會入隊。若如此,公平鎖將不再公平。

3.3 釋放鎖

分析完了獲取鎖的相關邏輯,接下來再來分析一下釋放鎖的邏輯。與獲取鎖相比,釋放鎖的邏輯會簡單一些,因為釋放鎖的過程沒有公平和非公平之分。好了,下面開始分析 unlock 的邏輯:

+--- ReentrantLock
public void unlock() {
    // 呼叫 AQS 中的 release 方法
    sync.release(1);
}

+--- AbstractQueuedSynchronizer
public final boolean release(int arg) {
    // 呼叫 ReentrantLock.Sync 中的 tryRelease 嘗試釋放鎖
    if (tryRelease(arg)) {
        Node h = head;
        /*
         * 如果頭結點的等待狀態不為0,則應該喚醒頭結點的後繼節點。
         * 這裡簡單說個結論:
         *     頭結點的等待狀態為0,表示頭節點的後繼節點執行緒還是活躍的,無需喚醒
         */
        if (h != null && h.waitStatus != 0)
            // 喚醒頭結點的後繼節點,該方法的分析請參考我寫的關於 AQS 的文章
            unparkSuccessor(h);
        return true;
    }
    return false;
}

+--- ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
    /*
     * 用同步狀態量 state 減去釋放量 releases,得到本次釋放鎖後的同步狀態量。
     * 當將 state 為 0,鎖才能被完全釋放
     */ 
    int c = getState() - releases;
    // 檢測當前執行緒是否已經持有鎖,僅允許持有鎖的執行緒執行鎖釋放邏輯
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
        
    boolean free = false;
    // 如果 c 為0,則表示完全釋放鎖了,此時將持鎖執行緒設為 null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    
    // 設定新的同步狀態
    setState(c);
    return free;
}

重入鎖的釋放邏輯並不複雜,這裡就不多說了。

4.總結

本文分析了可重入鎖 ReentrantLock 公平與非公平獲取鎖預計釋放鎖原理,並與 synchronized 關鍵字進行了類比。總體來說,ReentrantLock 的原理在熟悉 AQS 原理的情況下,理解並不是很複雜。ReentrantLock 是大家經常使用的一個同步元件,還是很有必要去弄懂它的原理的。

http://news.zce9839.cn/
http://news.gjc9646.cn/
http://news.myo1179.cn/
http://news.ogr7085.cn/
http://news.bah1564.cn/
http://news.mjg4415.cn/
http://news.dkk2480.cn/
http://news.qru6126.cn/
http://news.ocs5821.cn/
http://news.wne9476.cn/
http://news.xuh4863.cn/
http://news.icb3050.cn/
http://news.tfe0886.cn/
http://news.xgs5975.cn/
http://news.umx9976.cn/
http://news.eyf3292.cn/
http://news.wxm6819.cn/
http://news.ewv7964.cn/
http://news.wdr5566.cn/
http://news.qdn5355.cn/
http://news.kpp1176.cn/
http://news.rxi1689.cn/
http://news.vja2045.cn/
http://news.qry8357.cn/
http://news.pck8038.cn/
http://news.hiv8337.cn/
http://news.bjl7141.cn/
http://news.qou5361.cn/
http://news.kgs1242.cn/
http://news.zri8413.cn/
http://news.vpk8803.cn/
http://news.kwx0094.cn/
http://news.kft8401.cn/
http://news.bbe1708.cn/
http://news.nne0088.cn/
http://news.wph2022.cn/
http://news.uvr1927.cn/
http://news.qfg6726.cn/
http://news.ffr2858.cn/
http://news.rlp0976.cn/
http://news.qsa5453.cn/
http://news.rgj9684.cn/
http://news.xrr9518.cn/
http://news.dwm8256.cn/
http://news.yhb3879.cn/
http://news.evv5980.cn/
http://news.nat5354.cn/
http://news.nkc4539.cn/
http://news.jal3249.cn/
http://news.ppr6189.cn/
http://news.xnw9449.cn/
http://news.jwv1856.cn/
http://news.ddh4684.cn/
http://news.ymu2666.cn/
http://news.kdz0246.cn/
http://news.sph9900.cn/

相關文章