全網最詳細的ReentrantReadWriteLock原始碼剖析(萬字長文)

酒冽發表於2021-12-07

碎碎念)
花了兩天時間,終於把ReentrantReadWriteLock(讀寫鎖)解析做完了。之前鑽研過AQS(AbstractQueuedSynchronizer)的原始碼,弄懂讀寫鎖也沒有想象中那麼困難。而且閱讀完ReentrantReadWriteLock的原始碼,正好可以和AQS的原始碼串起來理解,相輔相成。後面博主會盡快把AQS的原始碼解析整出來

簡介

ReentrantReadWriteLock是一個可重入讀寫鎖,內部提供了讀鎖寫鎖的單獨實現。其中讀鎖用於只讀操作,可被多個執行緒共享;寫鎖用於寫操作,只能互斥訪問

ReentrantReadWriteLock尤其適合讀多寫少的應用場景

讀多寫少:
在一些業務場景中,大部分只是讀資料,寫資料很少,如果這種場景下依然使用獨佔鎖(如synchronized),會大大降低效能。因為獨佔鎖會使得本該並行執行的讀操作,變成了序列執行

ReentrantReadWriteLock實現了ReadWriteLock介面,該介面只有兩個方法,分別用於返回讀鎖和寫鎖,這兩個鎖都是Lock物件。該介面原始碼如下:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReentrantReadWriteLock有兩個域,分別存放讀鎖和寫鎖:

private final ReentrantReadWriteLock.ReadLock readerLock;

private final ReentrantReadWriteLock.WriteLock writerLock;

ReentrantReadWriteLock的核心原理主要在於兩點:

  • 內部類Sync:其實現了的AQS大部分方法。Sync類有兩個子類FairSync和NonfairSync,分別實現了公平讀寫鎖非公平讀寫鎖。Sync類及其子類的原始碼解析會在後面逐步給出
  • 內部類ReadLock和WriteLock:分別是讀鎖和寫鎖的具體實現,它們都和ReentrantLock一樣實現了Lock介面,因此實現的手段也和ReentrantLock一樣,都是委託給內部的Sync類物件來實現,對應的原始碼解析也會在後面給出

ReentrantReadWriteLock的特點

讀寫鎖的互斥關係

  • 讀鎖和寫鎖之間是互斥模式:當有執行緒持有讀鎖時,寫鎖不能獲得;當有別的執行緒持有寫鎖時,讀鎖不能獲得
  • 讀鎖和讀鎖之間是共享模式
  • 寫鎖和寫鎖之間是互斥模式

可重入性

ReentrantReadWriteLock在ReadWriteLock介面之上,新增了可重入的特性,且讀鎖和寫鎖都支援可重入

  • 如果一個執行緒獲取了讀鎖,那麼它可以再次獲取讀鎖(但直接獲取寫鎖會失敗,原因見下方的“鎖的升降級”)
  • 如果一個執行緒獲取了寫鎖,那麼它可以再次獲取寫鎖或讀鎖

鎖的升降級

鎖升級

ReentrantReadWriteLock不支援鎖升級,即同一個執行緒獲取讀鎖後,直接申請寫鎖是不能獲取成功的。測試程式碼如下:

public class Test1 {
    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
        rtLock.readLock().lock();
        System.out.println("get readLock.");
        rtLock.writeLock().lock();
        System.out.println("blocking");
    }
}

執行到第6行會因為獲取失敗而被阻塞,導致Test1發生死鎖。命令列輸出如下:

get readLock.

鎖降級

ReentrantReadWriteLock支援鎖降級,即同一個執行緒獲取寫鎖後,直接申請讀鎖是可以直接成功的。測試程式碼如下:

public class Test2 {
    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();  
        rtLock.writeLock().lock();  
        System.out.println("writeLock");  
        rtLock.readLock().lock();  
        System.out.println("get read lock");  
    }
}

該程式不會產生死鎖。結果輸出如下:

writeLock
get read lock

Process finished with exit code 0

讀寫鎖的升降級規則總結

  • ReentrantReadWriteLock不支援鎖升級,因為可能有其他執行緒同時持有讀鎖,而讀寫鎖之間是互斥的,存在衝突‘
  • ReentrantReadWriteLock支援鎖降級,因為如果該執行緒持有寫鎖時,一定沒有其他執行緒能夠持有讀鎖或寫鎖的,因此降級為讀鎖不存在衝突

公平鎖和非公平鎖

ReentrantReadWriteLock支援公平模式和非公平模式獲取鎖。從效能上來看,非公平模式更好
二者的規則如下:

  • 公平鎖:無論是讀執行緒還是寫執行緒,在申請鎖時都會檢查是否有其他執行緒在同步佇列中等待。如果有,則讓步
  • 非公平鎖:如果是讀執行緒,在申請鎖時會判斷是否有寫執行緒在同步佇列中等待。如果有,則讓步;如果是寫執行緒,則直接競爭鎖資源,不會在乎別的執行緒

Sync類

Sync是一個抽象類,有兩個具體子類NonfairSync和FairSync,分別對應非公平讀寫鎖公平讀寫鎖。Sync類的主要作用就是為這兩個子類提供絕絕絕大部分的方法實現
只定義了兩個抽象方法writerShouldBlock和readerShouldBlocker交給兩個子類去實現,太寵了吧-_-||

讀狀態和寫狀態

Sync利用AQS單個state,同時表示讀狀態和寫狀態,原始碼如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 6317671515068378041L;

    /*
    * Read vs write count extraction constants and functions.
    * Lock state is logically divided into two unsigned shorts:
    * The lower one representing the exclusive (writer) lock hold count,
    * and the upper the shared (reader) hold count.
    */

    static final int SHARED_SHIFT   = 16;
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    
    /** Returns the number of shared holds represented in count  */
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    /** Returns the number of exclusive holds represented in count  */
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    
    // ······
    
};

根據上面原始碼可以看出:

  • SHARED_SHIFT表示AQS中的state(int型,32位)的高16位,作為“讀狀態”,低16位作為“寫狀態
  • SHARED_UNIT二級製為2^16,讀鎖加1,state加SHARED_UNIT
  • MAX_COUNT就是寫或讀資源的最大數量,為2^16-1
  • 使用sharedCount方法獲取讀狀態,使用exclusiveCount方法獲取獲取寫狀態

state劃分為讀、寫狀態的示意圖如下,其中讀鎖持有1個,寫鎖持有3個:
image

記錄首個獲得讀鎖的執行緒

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

firstReader記錄首個獲得讀鎖的執行緒;firstReaderHoldCount記錄其持有的讀鎖數

執行緒區域性計數器

Sync類定義了一個執行緒區域性變數readHolds,用於儲存當前執行緒重入讀鎖的次數。如果該執行緒的讀鎖數減為0,則將該變數從執行緒區域性域中移除。相關原始碼如下:

// 內部類,用於記錄當前執行緒重入讀鎖的次數
static final class HoldCounter {
    int count = 0;
    // 這裡使用執行緒的id而非直接引用,是為了方便GC
    final long tid = getThreadId(Thread.currentThread());
}

// 內部類,繼承ThreadLocal,該型別的變數是每個執行緒各自儲存一份,其中儲存的是HoldCounter物件,用set方法儲存,get方法獲取
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

private transient ThreadLocalHoldCounter readHolds;

由於readHolds變數是執行緒區域性變數(繼承ThreadLocal類),每個執行緒都會儲存一份副本,不同執行緒呼叫其get方法返回的HoldCounter物件不同

readHolds中的HoldCounter變數儲存了每個讀執行緒的重入次數,即其持有的讀鎖數量。這麼做的目的是便於執行緒釋放讀鎖時進行合法性判斷:執行緒在不持有讀鎖的情況下釋放鎖是不合法的,需要丟擲IllegalMonitorStateException異常

快取

Sync類定義了一個HoldCounter變數cachedHoldCounter,用於儲存最近獲取到讀鎖的執行緒的重入次數。原始碼如下:

// 這是一個啟發式演算法
private transient HoldCounter cachedHoldCounter;

設計該變數的目的是:將其作為一個快取,加快程式碼執行速度。因為獲取、釋放讀鎖的執行緒往往都是最近獲取讀鎖的那個執行緒,雖然每個執行緒的重入次數都會使用readHolds來儲存,但使用readHolds變數會涉及到ThreadLocal內部的查詢(lookup),這是存在一定開銷的。有了cachedHoldCounter這個快取後,就不用每次都在ThreadLocal內部查詢,加快了程式碼執行速度。相當於用空間換時間

獲取鎖

無論是公平鎖還是非公平鎖,它們獲取鎖的邏輯都是相同的,因此Sync類在這一層就提供了統一的實現

但是,獲取寫鎖和獲取讀鎖的邏輯不相同:

  • 寫鎖是互斥資源,獲取寫鎖的邏輯主要在tryAcquire方法
  • 讀鎖是共享資源,獲取讀鎖的邏輯主要在tryAcquireShared方法

具體的原始碼分析見下方的“讀鎖”和“寫鎖”各自章節的“獲取x鎖”部分

釋放鎖

無論是公平鎖還是非公平鎖,它們釋放鎖的邏輯都是相同的,因此Sync類在這一層就提供了統一的實現

但是,釋放寫鎖和釋放讀鎖的邏輯不相同:

  • 寫鎖是互斥資源,釋放寫鎖的邏輯主要在tryRelease方法
  • 讀鎖是共享資源,釋放讀鎖的邏輯主要在tryReleaseShared方法

具體的原始碼分析見下方的“讀鎖”和“寫鎖”各自章節的“釋放x鎖”部分

寫鎖

寫鎖是由內部類WriteLock實現的,其實現了Lock介面,獲取鎖、釋放鎖的邏輯都委託給了Sync類例項sync來執行。WriteLock的基本結構如下:

public static class WriteLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -4992448646407690164L;
    private final Sync sync;

    // 構造方法注入Sync類物件
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    
    // 實現了Lock介面的所有方法
}

獲取寫鎖

WriteLock使用lock方法獲取寫鎖,一次獲取一個寫鎖,原始碼如下:

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

lock方法內部實際呼叫的是AQS的acquire方法,原始碼如下:

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

而acquire方法會呼叫子類Sync實現的tryAcquire方法,如下:

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;		// 如果是讀鎖被獲取中,或寫鎖被獲取但不是本執行緒獲取的,則獲取失敗
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;		// 如果根據公平性判斷此時寫執行緒需要被阻塞,或在獲取過程中發生競爭且競爭失敗,則獲取失敗
    setExclusiveOwnerThread(current);
    return true;
}

分為三步:
1、如果讀鎖正在被獲取中,或者寫鎖被獲取中但不是本執行緒持有,則獲取失敗
2、如果獲取寫鎖達到飽和,則丟擲異常
3、如果上面兩個都不成立,說明此執行緒可以請求寫鎖。但需要先根據公平性來判斷是否應該先阻塞。如果不用阻塞,且CAS成功,則獲取成功。否則獲取失敗

其中公平性判斷所呼叫的writerShouldBlock,在後面分析公平性鎖和非公平性鎖時會解析

如果tryAcquire方法獲取寫鎖成功,則acquire方法直接返回,否則進入同步佇列阻塞等待

tryAcquire體現的讀寫鎖的特徵:

  • 互斥關係:
    • 寫鎖和寫鎖之間是互斥的:如果是別的執行緒持有寫鎖,那麼直接返回false
    • 讀鎖和寫鎖之間是互斥的。當有執行緒持有讀鎖時,寫鎖不能獲得:如果c!=0且w==0,說明此時有執行緒持有讀鎖,直接返回false
  • 可重入性:如果當前執行緒持有寫鎖,就不用進行公平性判斷writerShouldBlock,請求鎖一定會獲取成功
  • 不允許鎖升級:如果當前執行緒持有讀鎖,想要直接申請寫鎖,此時c!=0且w==0,而exclusiveOwnerThread是null,不等於current,直接返回false

釋放寫鎖

WriteLock使用unlock方法釋放寫鎖,如下:

public void unlock() {
    sync.release(1);
}

unlock內部實際上呼叫的是AQS的release方法,原始碼如下:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

而該方法會呼叫子類Sync實現的tryAcquire方法,原始碼如下:

protected final boolean tryRelease(int releases) {
    // 如果並不持有鎖就釋放,會丟擲異常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    
    // 如果釋放鎖之後鎖空閒,那麼需要將鎖持有者置為null
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;		// 返回鎖釋放後是否空閒
}

注意:
任何鎖得釋放都需要判斷是否是在持有鎖的情況下。如果不持有鎖就釋放,會丟擲異常。對於寫鎖來說,判斷是否持有鎖很簡單,只需要呼叫isHeldExclusively;而對於讀鎖來說,判斷是否持有鎖比較複雜,需要根據每個執行緒各自儲存的持有讀鎖數來判斷,即readHolds中儲存的變數

嘗試獲取寫鎖

WriteLock使用tryLock來嘗試獲取寫鎖,如下:

public boolean tryLock( ) {
    return sync.tryWriteLock();
}

tryLock內部實際呼叫的是Sync類定義並實現的tryWriteLock方法。該方法是一個final方法,不允許子類重寫。其原始碼如下:

final boolean tryWriteLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c != 0) {
        int w = exclusiveCount(c);
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    if (!compareAndSetState(c, c + 1))		// 相比於tryAcquire方法,這裡缺少對公平性判斷(writerShouldBlock)
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

其實除了缺少對公平性判斷方法writerShouldBlock的呼叫以外,和tryAcquire方法基本上是一樣的,這裡不再廢話

Lock介面其他方法的實現

// 支援中斷響應的lock方法,實際上呼叫的是AQS的acquireInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

// 實際上呼叫的是AQS的方法tryAcquireNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

// 實際上呼叫的是Sync類實現的newCondition方法
public Condition newCondition() {
    return sync.newCondition();
}

寫鎖是支援建立條件變數的,因為寫鎖是獨佔鎖,而條件變數在await時會釋放掉所有鎖資源。寫鎖能夠保證所有的鎖資源都是本執行緒所持有,所以可以放心地去釋放所有鎖

而讀鎖不支援建立條件變數,因為讀鎖是共享鎖,可能會有其他執行緒持有讀鎖。如果呼叫await,不僅會釋放掉本執行緒持有的讀鎖,也會釋放掉其他執行緒持有的讀鎖,這是不被允許的。因此讀鎖不支援條件變數

讀鎖

讀鎖是由內部類ReadLock實現的,其實現了Lock介面,獲取鎖、釋放鎖的邏輯都委託給了Sync類例項sync來執行。ReadLock的基本結構如下:

public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    private final Sync sync;

    // 構造方法注入Sync類物件
    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
	
    // 實現了Lock介面的所有方法
}

獲取讀鎖

ReadLock使用lock方法獲取讀鎖,一次獲取一個讀鎖。原始碼如下:

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

lock方法內部實際呼叫的是AQS的acquireShared方法,原始碼如下:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

該方法會呼叫Sync類實現的tryAcquireShared方法,原始碼如下:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;			// 如果寫鎖被獲取,且並不是由本執行緒持有寫鎖,那麼獲取失敗
    int r = sharedCount(c);
    if (!readerShouldBlock() &&		// 先進行公平性判斷是否應該讓步,這可能會導致重入讀鎖的執行緒獲取失敗
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {		// CAS失敗可能也會導致本能獲取成功的執行緒獲取失敗
        // 如果此時讀鎖沒有被獲取,則該執行緒是第一個獲取讀鎖的執行緒,記錄相應資訊
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        }
        // 該執行緒不是首個獲取讀鎖的執行緒,需要記錄到readHolds中
        else {
            HoldCounter rh = cachedHoldCounter;		// 通常當前獲取讀鎖的執行緒就是最近獲取到讀鎖的執行緒,所以直接用快取
            // 還是需要判斷一下是不是最近獲取到讀鎖的執行緒。如果不是,則呼叫get建立一個新的區域性HoldCounter變數
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            // 之前最近獲取讀鎖的執行緒如果釋放完了讀鎖而導致其區域性HoldCounter變數被remove了,這裡重新獲取就重新set
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;	// 如果公平性判斷無需讓步,且讀鎖數未飽和,且CAS競爭成功,則說明獲取成功
    }
    return fullTryAcquireShared(current);
}

tryAcquireShared的返回值說明:

  • 負數:獲取失敗,執行緒會進入同步佇列阻塞等待
  • 0:獲取成功,但是後續以共享模式獲取的執行緒都不可能獲取成功(這裡暫時用不上)
  • 正數:獲取成功,且後續以共享模式獲取的執行緒也可能獲取成功

tryAcquireShared沒有返回0的情況,只會返回正數或負數

前面“Sync類”中講解過這些變數,這裡再複習一遍:

  • firstReader、firstReaderHoldCount分別用於記錄第一個獲取到寫鎖的執行緒及其持有讀鎖的數量
  • cachedHoldCounter用於記錄最後一個獲取到寫鎖的執行緒持有讀鎖的數量
  • readHolds是一個執行緒區域性變數(ThreadLocal變數),用於儲存每個獲得讀鎖的執行緒各自持有的讀鎖數量

tryAcquireShared的流程如下:
1、如果其他執行緒持有寫鎖,那麼獲取失敗(返回-1)
2、否則,根據公平性判斷是否應該阻塞。如果不用阻塞且讀鎖數量未飽和,則CAS請求讀鎖。如果CAS成功,獲取成功(返回1),並記錄相關資訊
3、如果根據公平性判斷應該阻塞,或者讀鎖數量飽和,或者CAS競爭失敗,那麼交給完整版本的獲取方法fullTryAcquireShared去處理

其中步驟2如果發生了重入讀(當前執行緒持有讀鎖的情況下,再次請求讀鎖),但根據公平性判斷該執行緒需要阻塞等待,而導致重入讀失敗。按照正常邏輯,重入讀不應該失敗。不過,tryAcquireShared並沒有處理這種情況,而是將其放到了fullTryAcquireShared中進行處理。此外,CAS競爭失敗而導致獲取讀鎖失敗,也交給fullTryAcquireShared去處理(fullTryAcquireShared表示我好難)

fullTryAcquireShared方法是嘗試獲取讀鎖的完全版本,用於處理tryAcquireShared方法未處理的:
1、CAS競爭失敗
2、因公平性判斷應該阻塞而導致的重入讀失敗
這兩種情況。其原始碼如下:

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // 如果當前執行緒就是firstReader,那麼它一定是重入讀,不讓它失敗,而是重新loop直到公平性判斷不阻塞為止
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)		// 當前執行緒既不持有讀鎖(不是重入讀),並且被公平性判斷為應該阻塞,那麼就獲取失敗
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        
        // 下面的邏輯基本上和tryAcquire中差不多,不過這裡的CAS如果失敗,會重新loop直到成功為止
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

fullTryAcquireShared其實和tryAcquire存在很多的冗餘之處,但這麼做的目的主要是讓tryAcquireShared變得更簡單,不用處理複雜的CAS迴圈

fullTryAcquireShared主要是為了處理CAS失敗和readerShouldBlock判true而導致的重入讀失敗,這兩種情況在理論上都應該成功獲取鎖。fullTryAcquireShared的做法就是將這兩種情況放在for迴圈中,一旦發生就重新迴圈,直到成功為止

tryAcquireShared和fullTryAcquireShared體現的讀寫鎖特徵:

  • 互斥關係:
    • 讀鎖和讀鎖之間是共享的:即使有其他執行緒持有了讀鎖,當前執行緒也能獲取讀鎖
    • 讀鎖和寫鎖之間是互斥的。當有別的執行緒持有寫鎖,讀鎖不能獲得:tryAcquireShared第4-6行,fullTryAcquireShared第5-7行都能體現這一特徵
  • 可重入性:如果當前執行緒獲取了讀鎖,那麼它再次申請讀鎖一定能成功。這部分邏輯是由fullTryAcquireShared的for迴圈實現的
  • 支援鎖降級:如果當前執行緒持有寫鎖,那麼它申請讀鎖一定會成功。這部分邏輯見tryAcquireShared第5行,current和exclusiveOwnerThread是相等的,不會返回-1

釋放讀鎖

ReadLock使用unlock方法釋放讀鎖,如下:

public void unlock() {
    sync.releaseShared(1);
}

unlock方法實際呼叫的是AQS的releaseShared方法,如下:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

而該方法會呼叫Sync類實現的tryReleaseShared方法,原始碼如下:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;		// 一般釋放鎖的都是最後獲取鎖的那個執行緒
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();		// 如果釋放讀鎖後不再持有鎖,那麼移除readHolds儲存的執行緒區域性HoldCounter變數
            if (count <= 0)
                throw unmatchedUnlockException();	// 丟擲IllegalMonitorStateException異常
        }
        --rh.count;
    }
    // 迴圈CAS保證修改state成功
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;		// 如果釋放後鎖空閒,那麼返回true,否則返回false
    }
}

如果返回true,說明鎖是空閒的,releaseShared方法會進一步呼叫doReleaseShared方法,doReleaseShared方法會喚醒後繼執行緒並確保傳播(確保傳播:保證被喚醒的執行緒可以執行喚醒其後續執行緒的邏輯

嘗試釋放讀鎖

ReadLock使用tryLock方法嘗試釋放讀鎖,原始碼如下:

public boolean tryLock() {
    return sync.tryReadLock();
}

tryLock內部實際呼叫的是Sync類定義並實現的tryReadLock方法。該方法是一個final方法,不允許子類重寫。其原始碼如下:

final boolean tryReadLock() {
    Thread current = Thread.currentThread();
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return false;
        int r = sharedCount(c);
        if (r == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return true;
        }
    }
}

其實除了缺少對公平性判斷方法readerShouldBlock的呼叫以外,和tryAcquireShared方法基本上是一樣的

Lock介面其他方法的實現

// 支援中斷響應的lock方法,實際上呼叫的是AQS的acquireSharedInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// 實際上呼叫的是AQS的方法tryAcquireSharedNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

// 讀鎖不支援建立條件變數
public Condition newCondition() {
    throw new UnsupportedOperationException();
}

和寫鎖的區別在於,讀鎖不支援建立條件變數。如果呼叫newCondition方法,會直接丟擲UnsupportedOperationException異常。不支援的原因在前面已經分析過,這裡不再贅述

讀寫鎖的公平性

公平讀寫鎖

ReentrantReadWriteLock預設構造方法如下:

public ReentrantReadWriteLock() {
    this(false);
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

可見其預設建立的是非公平讀寫鎖

公平讀寫鎖依賴於Sync的子類FairSync實現,其原始碼如下:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

writerShouldBlock

writerShouldBlock實際上呼叫的是AQS的hasQueuedPredecessors方法,該方法會檢查是否有執行緒在同步佇列中等待,原始碼如下:

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // 如果head等於tail,說明是空佇列
    // 如果隊首的thread域不是當前執行緒,說明有別的執行緒先於當前執行緒等待獲取鎖
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

writerShouldBlock只有在tryAcquire中被呼叫。如果當前執行緒請求寫鎖時發現已經有執行緒(讀執行緒or寫執行緒)在同步佇列中等待,則讓步

readerShouldBlock

readerShouldBlock和writerShouldBlock一樣,都是呼叫AQS的hasQueuedPredecessors方法

readerShouldBlock只有在tryAcquireShared(fullTryAcquireShared)中被呼叫。如果當前執行緒請求讀鎖時發現已經有執行緒(讀執行緒or寫執行緒)在同步佇列中等待,則讓步

非公平讀寫鎖

如果要建立非公平讀寫鎖,需要使用有參建構函式,引數fair設定為true

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

非公平讀寫鎖依賴於Sync的子類NonfairSync實現,其原始碼如下:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}

writerShouldBlock

writerShouldBlock直接返回false

writerShouldBlock只有在tryAcquire中被呼叫,返回false表示在非公平模式下,不管是否有執行緒在同步佇列中等待,請求寫鎖都不會讓步,而是直接上去競爭

readerShouldBlock

readerShouldBlock實際呼叫的是AQS的apparentlyFirstQueuedIsExclusive方法。其原始碼如下:

final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

如果同步佇列為空,或隊首執行緒是讀執行緒(獲取讀鎖而被阻塞),則返回false。如果同步佇列隊首執行緒是寫執行緒(獲取寫鎖而被阻塞),則返回true

readerShouldBlock只有在tryAcquireShared(fullTryAcquireShared)中被呼叫。如果當前執行緒請求讀鎖時發現同步佇列隊首執行緒是寫執行緒,則讓步。如果是讀執行緒則跟它爭奪鎖資源

這麼做的目的是為了防止寫執行緒被“餓死”。因為如果一直有讀執行緒前來請求鎖,且讀鎖是有求必應,就會使得在同步佇列中的寫執行緒一直不能被喚醒。不過,apparentlyFirstQueuedIsExclusive只是一種啟發式演算法,並不能保證寫執行緒一定不會被餓死。因為寫執行緒有可能不在同步佇列隊首,而是排在其他讀執行緒後面

讀寫鎖的公平性總結

公平模式:
無論當前執行緒請求寫鎖還是讀鎖,只要發現此時還有別的執行緒在同步佇列中等待(寫鎖or讀鎖),都一律選擇讓步

非公平模式:

  • 請求寫鎖時,當前執行緒會選擇直接競爭,不會做絲毫的讓步
  • 請求讀鎖時,如果發現同步佇列隊首執行緒在等待獲取寫鎖,則會讓步。不過這是一種啟發式演算法,因為寫執行緒可能排在其他讀執行緒後面

如果覺得作者寫的還可以的話,可以?鼓勵一下,嘻嘻

相關文章