天天看點

ReentrantReadWriteLock的實作原理與鎖擷取

1.面試題分析

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

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

有點類似MySQL資料庫為代表的讀寫分離機制,既然我們知道了讀寫鎖是用于讀多寫少的場景。那問題來了,ReentrantReadWriteLock是怎樣來實作的呢,它與ReentrantLock的實作又有什麼的差別呢?

2.ReentrantReadWriteLock簡介

​ 很多情況下有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁。

​ 在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,是以應該允許多個線程同時讀取共享資源,但是如果一個線程想去寫這些共享資源,就不應該允許其他線程對該資源進行讀和寫的操作了。

​ 針對這種場景,JAVA的并發包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操作相關的鎖,稱為共享鎖;一個是寫相關的鎖,稱為排他鎖。

3.ReentrantReadWriteLock特性

公平性:讀寫鎖支援非公平和公平的鎖擷取方式,非公平鎖的吞吐量優于公平鎖的吞吐量,預設構造的是非公平鎖

可重入:線上程擷取讀鎖之後能夠再次擷取讀鎖,但是不能擷取寫鎖,而線程在擷取寫鎖之後能夠再次擷取寫鎖,同時也能擷取讀鎖

鎖降級:線程擷取寫鎖之後擷取讀鎖,再釋放寫鎖,這樣實作了寫鎖變為讀鎖,也叫鎖降級

4.ReentrantReadWriteLock的主要成員和結構圖

  1. ReentrantReadWriteLock的繼承關系

ReentrantReadWriteLock的實作原理與鎖擷取

public interface ReadWriteLock {

Lock writeLock();

}

讀寫鎖 ReadWriteLock

​ 讀寫鎖維護了一對相關的鎖,一個用于隻讀操作,一個用于寫入操作。

​ 隻要沒有寫入,讀取鎖可以由多個讀線程同時保持,寫入鎖是獨占的。

2.ReentrantReadWriteLock的核心變量

ReentrantReadWriteLock的實作原理與鎖擷取

ReentrantReadWriteLock類包含三個核心變量:

ReaderLock:讀鎖,實作了Lock接口

WriterLock:寫鎖,也實作了Lock接口

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);
    }
           

4.ReentrantReadWriteLock的核心實作

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

讀寫狀态的設計

寫鎖的擷取與釋放

讀鎖的擷取與釋放

鎖降級

1.讀寫狀态的設計

​ 之前談ReentrantLock的時候,Sync類是繼承于AQS,主要以int state為線程鎖狀态,0表示沒有被線程占用,1表示已經有線程占用。

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

​ 如果在一個整型變量上維護多種狀态,就一定需要“按位切割使用”這個變量,ReentrantReadWriteLock将int類型的state将變量切割成兩部分:

高16位記錄讀鎖狀态

低16位記錄寫鎖狀态

ReentrantReadWriteLock的實作原理與鎖擷取
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才能擷取寫鎖進行資料更新。