Java 重入鎖 ReentrantLock 原理分析

1.簡介

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

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

2.原理

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

2.1 與 synchronized 的異同

ReentrantLock 和 synchronized 都是用於線程的同步控制,但它們在功能上來講差異仍是很大的。對比下來 ReentrantLock 功能明顯要豐富的多。下面簡單列舉一下二者之間的差別,以下:多線程

特性 synchronized ReentrantLock 相同
可重入
響應中斷
超時等待
公平鎖
非公平鎖
是否可嘗試加鎖
是不是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,並再次得到鎖,而不會被阻塞住。示例代碼大體以下:ui

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 被重入的次數了。這就是可重複特性的大體實現流程。spa

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 是你們常用的一個同步組件,仍是頗有必要去弄懂它的原理的。

好了,本文到這裏就結束了。謝謝你們的閱讀,再見。

參考

本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處註明出處
做者:coolblog
本文同步發佈在個人我的博客: http://www.coolblog.xyz

cc
本做品採用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。

相關文章
相關標籤/搜索