Java 讀寫鎖 ReadWriteLock 原理與應用場景詳解

2022-09-30 12:02:20

Java並行程式設計提供了讀寫鎖,主要用於讀多寫少的場景,今天我就重點來講解讀寫鎖的底層實現原理@mikechen

什麼是讀寫鎖?

讀寫鎖並不是JAVA所特有的讀寫鎖(Readers-Writer Lock)顧名思義是一把鎖分為兩部分:讀鎖和寫鎖,其中讀鎖允許多個執行緒同時獲得,因為讀操作本身是執行緒安全的,而寫鎖則是互斥鎖,不允許多個執行緒同時獲得寫鎖,並且寫操作和讀操作也是互斥的。

所謂的讀寫鎖(Readers-Writer Lock),顧名思義就是將一個鎖拆分為讀鎖和寫鎖兩個鎖。

其中讀鎖允許多個執行緒同時獲得,而寫鎖則是互斥鎖,不允許多個執行緒同時獲得寫鎖,並且寫操作和讀操作也是互斥的。

 

為什麼需要讀寫鎖?

Synchronized 和 ReentrantLock 都是獨佔鎖,即在同一時刻只有一個執行緒獲取到鎖。

然而在有些業務場景中,我們大多在讀取資料,很少寫入資料,這種情況下,如果仍使用獨佔鎖,效率將及其低下。

針對這種情況,Java提供了讀寫鎖——ReentrantReadWriteLock。

主要解決:對共用資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁的場景。

 

讀寫鎖的特點

  • 公平性:讀寫鎖支援非公平和公平的鎖獲取方式,非公平鎖的吞吐量優於公平鎖的吞吐量,預設構造的是非公平鎖
  • 可重入:線上程獲取讀鎖之後能夠再次獲取讀鎖,但是不能獲取寫鎖,而執行緒在獲取寫鎖之後能夠再次獲取寫鎖,同時也能獲取讀鎖
  • 鎖降級:執行緒獲取寫鎖之後獲取讀鎖,再釋放寫鎖,這樣實現了寫鎖變為讀鎖,也叫鎖降級

 

讀寫鎖的使用場景

ReentrantReadWriteLock適合讀多寫少的場景:

讀鎖ReentrantReadWriteLock.ReadLock可以被多個執行緒同時持有, 所以並行能力很高。

寫鎖ReentrantReadWriteLock.WriteLock是獨佔鎖, 在一個執行緒持有寫鎖時候, 其他執行緒都不能在搶佔, 包含搶佔讀鎖都會阻塞。

ReentrantReadWriteLock的使用場景總結:其實就是 讀讀並行、讀寫互斥、寫寫互斥而已,如果一個物件並行讀的場景大於並行寫的場景,那就可以使用 ReentrantReadWriteLock來達到保證執行緒安全的前提下提高並行效率。

 

讀寫鎖的主要成員和結構圖

1. ReentrantReadWriteLock的繼承關係

 

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

 

讀寫鎖 ReadWriteLock

讀寫鎖維護了一對相關的鎖,一個用於唯讀操作,一個用於寫入操作。

只要沒有寫入,讀取鎖可以由多個讀執行緒同時保持,寫入鎖是獨佔的。

 

2.ReentrantReadWriteLock的核心變數

ReentrantReadWriteLock類包含三個核心變數:

  1. ReaderLock:讀鎖,實現了Lock介面
  2. WriterLock:寫鎖,也實現了Lock介面
  3. Sync:繼承自AbstractQueuedSynchronize(AQS),可以為公平鎖FairSync 或 非公平鎖NonfairSync

3.ReentrantReadWriteLock的成員變數和建構函式

/** 內部提供的讀鎖 */

    private final ReentrantReadWriteLock.ReadLock readerLock;

    /** 內部提供的寫鎖 */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    /** AQS來實現的同步器 */
    final Sync sync;

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * 預設建立非公平的讀寫鎖
     */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * the given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

 

讀寫鎖的實現原理

ReentrantReadWriteLock實現關鍵點,主要包括:

  • 讀寫狀態的設計
  • 寫鎖的獲取與釋放
  • 讀鎖的獲取與釋放
  • 鎖降級

1.讀寫狀態的設計

之前談ReentrantLock的時候,Sync類是繼承於AQS,主要以int state為執行緒鎖狀態,0表示沒有被執行緒佔用,1表示已經有執行緒佔用。

同樣ReentrantReadWriteLock也是繼承於AQS來實現同步,那int state怎樣同時來區分讀鎖和寫鎖的?

如果在一個整型變數上維護多種狀態,就一定需要「按位元切割使用」這個變數,ReentrantReadWriteLock將int型別的state將變數切割成兩部分:

  • 高16位元記錄讀鎖狀態
  • 低16位元記錄寫鎖狀態

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 版本序列號
    private static final long serialVersionUID = 6317671515068378041L;        
    // 高16位元為讀鎖,低16位元為寫鎖
    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;
    // 本地執行緒計數器
    private transient ThreadLocalHoldCounter readHolds;
    // 快取的計數器
    private transient HoldCounter cachedHoldCounter;
    // 第一個讀執行緒
    private transient Thread firstReader = null;
    // 第一個讀執行緒的計數
    private transient int firstReaderHoldCount;
}

 

2.寫鎖的獲取與釋放

protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            //獲取獨佔鎖(寫鎖)的被獲取的數量
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                //1.如果同步狀態不為0,且寫狀態為0,則表示當前同步狀態被讀鎖獲取
                //2.或者當前擁有寫鎖的執行緒不是當前執行緒
                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)c是獲取當前鎖狀態,w是獲取寫鎖的狀態。

2)如果鎖狀態不為零,而寫鎖的狀態為0,則表示讀鎖狀態不為0,所以當前執行緒不能獲取寫鎖。或者鎖狀態不為零,而寫鎖的狀態也不為0,但是獲取寫鎖的執行緒不是當前執行緒,則當前執行緒不能獲取寫鎖。

3)寫鎖是一個可重入的排它鎖,在獲取同步狀態時,增加了一個讀鎖是否存在的判斷。

寫鎖的釋放與ReentrantLock的釋放過程類似,每次釋放將寫狀態減1,直到寫狀態為0時,才表示該寫鎖被釋放了。

3.讀鎖的獲取與釋放

protected final int tryAcquireShared(int unused) {
    for(;;) {
        int c = getState();
        int nextc = c + (1<<16);
        if(nextc < c) {
           throw new Error("Maxumum lock count exceeded");
        }
        if(exclusiveCount(c)!=0 && owner != Thread.currentThread())
           return -1;
        if(compareAndSetState(c,nextc))
           return 1;
    }
}

 

1)讀鎖是一個支援重進入的共用鎖,可以被多個執行緒同時獲取。

2)在沒有寫狀態為0時,讀鎖總會被成功獲取,而所做的也只是增加讀狀態(執行緒安全)

3)讀狀態是所有執行緒獲取讀鎖次數的總和,而每個執行緒各自獲取讀鎖的次數只能選擇儲存在ThreadLocal中,由執行緒自身維護。

讀鎖的每次釋放均減小狀態(執行緒安全的,可能有多個讀執行緒同時釋放鎖),減小的值是1<<16。

 

4.鎖降級

降級是指當前把持住寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

鎖降級過程中的讀鎖的獲取是否有必要,答案是必要的。主要是為了保證資料的可見性,如果當前執行緒不獲取讀鎖而直接釋放寫鎖,假設此刻另一個執行緒獲取的寫鎖,並修改了資料,那麼當前執行緒就步伐感知到執行緒T的資料更新,如果當前執行緒遵循鎖降級的步驟,那麼執行緒T將會被阻塞,直到當前執行緒使資料並釋放讀鎖之後,執行緒T才能獲取寫鎖進行資料更新。

 

5.讀鎖與寫鎖的整體流程

 

讀寫鎖總結

本篇詳細介紹了ReentrantReadWriteLock的特徵、實現、鎖的獲取過程,通過4個關鍵點的核心設計:

  • 讀寫狀態的設計
  • 寫鎖的獲取與釋放
  • 讀鎖的獲取與釋放
  • 鎖降級

從而才能實現:共用資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁的應用場景。

作者簡介

陳睿|mikechen,10年+大廠架構經驗,《BAT架構技術500期》系列文章作者,專注於網際網路架構技術。

閱讀mikechen的網際網路架構更多技術文章合集

Java並行|JVM|MySQL|Spring|Redis|分散式|高並行