ReentrantReadWriteLock實現源碼剖析

    ReentrantReadWriteLock提供了一個讀寫鎖的實現,並且有着ReentrantLock相似的語義。

簡介

非公平策略

    此模式下,讀寫鎖獲取的順序是不確定的,服從於可重入的限制。不公平鎖意味着持續的競爭可能會無限延遲一個或者更多的讀線程或者寫線程,但比公平鎖有更高的吞吐量。

公平策略
    此模式下,線程競爭鎖獲取會使用一個大致精確的FIFO的策略。當前鎖被釋放之後,或者等待最長的一條寫線程會獲取寫鎖,或者如果有一組讀線程等待比所有寫線程都要長的時間,那麼這組線程會獲取讀鎖。
    如果寫鎖已被獲取,或者有寫線程在等待的時候,單條線程嘗試獲取一個公平的讀鎖就會被阻塞。該線程將無法獲取讀鎖直到當前等待最長的寫線程獲取鎖並且寫鎖被釋放。當然,如果寫線程放棄等待,讓一個或者更多的讀線程作爲隊列中最長等待線程並且此時沒有寫鎖沒被獲取,那麼這些讀線程就會獲取讀鎖。

    除非讀鎖和寫鎖都沒有被獲取(意味着沒有等待線程),單條線程嘗試獲取一個公平的寫鎖就會被阻塞。(注意非阻塞方法ReadLock.tryLock和WriteLock.tryLock不會遵循這個公平策略並且在可能情況下,將忽略等待的線程,獲取鎖後馬上返回)。

可重入性

    鎖允許讀線程和寫線程重新獲取讀或者寫鎖。寫線程可以獲取讀寫,但反過來則不允許。在其它應用中,可重入性在寫鎖在調用或回調到使用讀鎖執行讀操作的方法裏變得很有用。如果讀線程嘗試獲取寫鎖,則該線程永遠也不會成功。

鎖降級

    可重入性也允許從寫鎖下降到讀鎖,這是通過獲取寫鎖,然後獲取讀鎖,接着釋放寫鎖達到。不過,從讀寫到寫鎖的升級是不可能的。

鎖獲取的中斷

    讀鎖和寫鎖都支持在鎖獲取過程中的中斷操作。

Condition支持
    寫鎖提供了Condition實現。讀鎖並不支持Condition實現。這是由於寫鎖是獨佔鎖,讀鎖屬於共享鎖。

具體實現

(1)WriteLock獲取鎖實現
    我們先來分析一下WriteLock的實現。
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }


    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    使用者調用方法writeLock獲取寫鎖對象,該函數返回writeLock成員變量,該變量在構造函數裏初始化,類WriteLock是一個公有靜態內部類,我們看看實現。
    public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;


        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }


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


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


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


        public Condition newCondition() {
            return sync.newCondition();
        }
     }
    我們集中來看看這幾個主要方法(爲了方便解析,一些次要的方法沒有列出來)。獲取寫鎖的時候,調用lock方法,該方法調用內部類Sync的acquire方法,而這個內部類Sync也是AbstractQueuedSynchronizer的子類,acquire方法會調用tryAcquire嘗試獲取鎖,獲取失敗進入內部FIFO等待隊列,直到獲取成功。
    因此我們來看看這個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");
            setState(c + acquires);
            return true;
        }
        if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
            return false;
        setExclusiveOwnerThread(current);
        return true;
    }
    函數首先調用getState獲取鎖狀態,這裏要說明一下,這個鎖狀態並不是和ReentrantLock一樣簡單的0和大於等於1表示鎖的獲取情況,而是把這個鎖狀態int值分成高半位和低半位的兩個unsigned shorts值,低半位表示獨佔鎖(寫鎖)的獲取數(包括可重入),高半位表示共享鎖(讀鎖)的獲取數。
    如果要分別獲取寫鎖和讀鎖的獲取數,可以通過以下方法
    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;


    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    SHARED_UNIT表示添加一個讀鎖獲取數時(高半位添加1),鎖狀態數要真正添加的數字。MAX_COUNT表示讀鎖和寫鎖的最大獲取數,SHARED_SHIFT和EXCLUSIVE_MASK可以方便通過位移和掩位獲取讀鎖和寫鎖數,sharedCount和exclusiveCount就是分別利用這兩個數字完成了位移獲取讀鎖獲取數以及掩位獲取寫鎖獲取數。

    我們來重新看回tryAcquire的實現。調用exclusiveCount獲取了獨佔數獲取數之後,如果鎖狀態數c不爲0,此時有可能會寫鎖重入,但也有可能是讀鎖已被獲取。因此還要繼續判斷,如果此時獨佔鎖數w爲0,則此時共享鎖數必定不爲0(共享鎖數高半位和獨佔鎖數低半位組成鎖狀態數),也就是有讀線程獲取了讀鎖,由於寫鎖是獨佔的,因此寫鎖獲取失敗,要返回false。如果獨佔鎖w不爲0,但此時當前線程並不是獲取獨佔鎖的線程,則獲取鎖也失敗,同樣返回false。另外還需要通過把當前獨佔鎖數與請求獨佔鎖數相加,如果大於MAX_COUNT則要拋出異常。如果都沒有問題,則可以調用setState添加當前鎖狀態數,表明這是一次重入的寫鎖,返回true。
    如果c爲0的時候,此時就輪到是否公平策略的判斷了,在這裏調用writerShouldBlock方法讓公平鎖和非公平鎖決定是否可以馬上獲取鎖,如果writerShouldBlock返回true則當前獲取失敗,寫鎖要阻塞,返回false的時候就調用CAS來重新設置鎖狀態數,CAS成功的話就設置當前線程爲獲取了獨佔鎖線程,返回true,CAS失敗就返回false表示獲取鎖失敗。
    以下分別來看看NonfairSync類(非公平策略)和FairSync類(公平策略)的writerShouldBlock函數的實現。
    //NonfairSync
    final boolean writerShouldBlock() {
        return false;
    }


    //FairSync
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    兩者的實現都很簡單,NonfairSync類的writerShouldBlock直接返回false表示寫鎖可以插隊獲取鎖,FairSync類的實現則是調用基類AQS的hasQueuedPredecessors方法來的判斷當前等待隊列裏是否有前繼結點在等待鎖,如果有則返回true,表示需要當前寫線程需要被阻塞。
    我們看回WriteLock的tryLock方法,tryLock方法與上面的lock相比,最大不同便是允許外來線程插隊獲取鎖。tryLock調用Sync類的tryWriteLock方法,看看該方法的實現。
  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))
            return false;
        setExclusiveOwnerThread(current);
        return true;
    }
    tryWriteLock的實現與tryAcquire大體相同,最大區別就是沒有了writerShouldBlock這個公平策略控制的判斷。
    這樣,writeLock的獲取鎖實現分析就結束,總體上來說,與之前的ReentrantLock的tryAcquire相比,增加了讀鎖被獲取的情況判斷,具體差別不大。接下來看看writeLock釋放鎖實現。

(2)WriteLock釋放鎖實現
    WriteLock釋放鎖方法unlock的實現也很簡單,調用了Sync的基類AQS的release方法,按照之前的分析,release會調用tryRelease來判斷是否可以釋放獨佔鎖。具體實現如下。
    protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;
        boolean free = exclusiveCount(nextc) == 0;
        if (free)
            setExclusiveOwnerThread(null);
        setState(nextc);
        return free;
    }
    首先是isHeldExclusively判斷,函數判斷當前線程是否獲取獨佔鎖的線程相等,如果返回false,則要拋出異常IllegalMonitorStateException。然後計算出釋放後的鎖狀態數,如果此狀態數的獨佔鎖數爲0,則會把當前獨佔鎖線程設爲null,然後重新設置鎖狀態數。
    tryRelease的實現並沒有太多要注意的地方,和ReentrantLock也沒有太大區別。接下來看看ReadLock的獲取鎖實現。

(3)ReadLock獲取鎖實現
    我們來看看ReadLock類的實現。
 public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;


        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }


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


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


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


        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
    }
    同樣,我們這裏省略一些次要方法,集中看看獲取鎖和釋放鎖的實現。與WriteLock對比,我們注意到newCondition方法拋出異常UnsupportedOperationException,這是讀鎖是共享的,Condition要求鎖必須在獨佔模式下才能使用。
    獲取鎖方法lock調用類Sync的acquireShared方法,這是AQS獲取共享模式鎖的方法,與acquire相比,該方法最大特點就是會使後繼等待共享鎖的結點獲得共享鎖。由於獲取共享鎖會調用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)) {
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != current.getId())
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        return fullTryAcquireShared(current);
    }
    首先,我們回憶一下基類AQS要求函數返回負值代表獲取讀鎖失敗;返回0表示本次獲取讀鎖成功,但不允許後繼讀線程獲取,也就是不會喚醒等待隊列裏後繼共享結點;返回正值表示本次獲取讀鎖成功,同時允許後繼讀線程獲取讀結點,因此會喚醒等待隊列裏後繼共享結點。
    重新看看tryAcquireShared函數,做了以下事情:
    1、調用exclusiveCount獲取獨佔鎖數,如果不爲0,則此時有寫線程獲取了獨佔鎖,此時如果當前線程也不是獲取獨佔鎖的線程,則讀鎖獲取失敗,返回-1表示獲取讀鎖失敗;
    2、此狀態下,線程便有獲取讀鎖。於是調用readerShouldBlock方法,由公平策略判斷是否應該入隊列,如果返回false表示可以插隊獲取,然後如果讀鎖數小於MAX_COUNT,另外CAS又成功把當前鎖狀態數的共享鎖數+1(再次提醒,共享鎖數在高半位),然後就是一個利用ThreadLocal類分別記錄每條讀線程獲取讀鎖的次數,此處待下面詳細分析。
    3、如果步驟2失敗,有可能出現多條讀線程獲取讀鎖,或者寫線程嘗試獲取寫鎖的過程,因此調用fullTryAcquireShared利用自旋確保併發修改狀態。

    我們先來分析利用ThreadLocal類記錄每條讀線程獲取讀鎖的次數的代碼段。爲了實現這個功能,並且考慮到無競爭條件下,以及讀鎖重入的情況,進行了對應的優化加速讀取。首先,此處涉及的成員變量有以下幾個:
    private transient ThreadLocalHoldCounter readHolds;
    //針對讀鎖重入的優化
    private transient HoldCounter cachedHoldCounter;
    //針對無競爭讀鎖狀態下優化
    private transient Thread firstReader = null;
    private transient int firstReaderHoldCount;
    第一個readHolds就是主要每條線程的本地線程變量,其中ThreadLocalHoldCounter實現如下:
    static final class HoldCounter {
        int count = 0;
        final long tid = Thread.currentThread().getId();
    }


    static final class ThreadLocalHoldCounter 
        extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
    ThreadLocal類實現了這樣一個功能:每條線程裏都保留對同一個ThreadLocal類的引用,但對每條線程的ThreadLocal類進行讀寫操作都不會影響其它線程,看起來像每條線程都擁有各自的ThreadLocal類一樣。
    具體實現主要是在每條線程裏創建不同的存儲類,然後在該存儲類裏保留對同一個ThreadLocal類引用,以其hash作爲key,並且同時把對應的ThreadLocal類的value類(在本實現中是HoldCounter)存儲到對應位置上,這樣調用ThreadLocal類來獲取value類的值時,就會利用ThreadLocal類的hash作爲key搜索對應的value,然後返回,這樣就實現類同一個ThreadLocal類對於不同線程返回不同的value類。

    我們重新看回實現,爲了儘量避免ThreadLocal類的查表操作,首先針對於讀鎖重入的情況,利用了cachedHoldcounter進行優化。cachedHoldCounter是上一次成功獲取讀鎖的線程的ThreadLocal類的value類,在每次設置於當前線程的讀鎖數之後,利用cached記錄,就可以避免如果下次相同線程再次重入獲取讀鎖時的ThreadLoacal類查表操作。
    firstReader是第一個獲取讀鎖的線程,firstReaderHoldCount便是這條線程的讀鎖獲取數。對於沒有發生競爭的獲取讀鎖操作(只有單條線程在獲取讀鎖),這兩個變量便可以免去ThreadLocal的查表操作。
    瞭解完變量的大致作用之後,我們再來看回之前tryAcquiredShared代碼的實現每條線程讀鎖數就會覺得很簡單,其實現如下:
  if (r == 0) {
        firstReader = current;
        firstReaderHoldCount = 1;
    } else if (firstReader == current) {
        firstReaderHoldCount++;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
            cachedHoldCounter = rh = readHolds.get();
        else if (rh.count == 0)
            readHolds.set(rh);
        rh.count++;
    }
    當讀鎖獲取數r爲0,意味着這是第一條獲取讀鎖的線程,於是就記錄當前線程爲firstReader,並且把firstReaderHoldCount=1;然後如果r不爲0,但firstReader等於當前線程,則表示第一條獲取讀鎖線程重入,於是把firstReaderHoldCount加上一;如果以上都不是,則表示這是另外一條線程嘗試獲取讀鎖,如果cachedHoldCounter爲null或者cachedHoldCounter表示的線程id不是和當前線程id相等,則表示cache失效,需要調用readHolds.get()獲取當前線程的HoldCounter(如果不存在則會創建一個新的HoldCounter),另外如果cache是當前線程的HoldCounter,但此時count爲0,則需要重新把cache設置回去,因爲後面的release釋放的時候會remove。這樣確保cache是當前線程的HoldCounter之後,把當前cahce的count值加一。

    我們再看看當tryAcquireShared遇到併發導致修改失敗時,調用fullTryAcquireShared的實現:
    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()) {
                // Make sure we're not acquiring read lock reentrantly
                if (firstReader == current) {
                    // assert firstReaderHoldCount > 0;
                } else {
                    if (rh == null) {
                        rh = cachedHoldCounter;
                        if (rh == null || rh.tid != current.getId()) {
                            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");
            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 != current.getId())
                        rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                    cachedHoldCounter = rh; // cache for release
                }
                return 1;
            }
        }
    }
    函數實現看上去有點複雜,但事實上只是tryAcquiredShared的邏輯基礎上,增加了循環重試以及延遲的讀鎖數記錄的邏輯。
    循環做裏以下事情
    1、首先判斷獨佔鎖數是否爲0,如果不爲0,並且當前線程不是獲取獨佔鎖線程,則返回負值1表示獲取讀鎖失敗。
    2、如果獨佔鎖數爲0,則調用readerShouldBlock判斷當前讀線程是否需要入隊。如果返回true,則要繼續進行判斷,因爲如果是已經獲得讀鎖的重入情況,則仍然必須讓此線程能夠獲得鎖,因此這裏先進行firstReader判斷,如果失敗,則繼續從cache獲取HoldCounter,確保rh是當前線程的HoldCounter以後,如果rh.count爲0,則表示不是重入情況,馬上返回負值1表示獲取讀鎖失敗。
    3、接着,如果讀鎖數是否等於MAX_COUNT,如果是則要拋出異常。
    4、到達這裏,就可以利用CAS更改當前鎖狀態數,如果成功來則是和tryAcquireShared類似的更新當前線程的獲取讀鎖數,這裏就不重複贅述。成功更改之後,返回正值1表示獲取讀鎖成功,並且後繼讀線程可以繼續獲取。如果CAS失敗,則必須重複以上步驟,保證併發操作能夠成功修改。

    接着來看看readerShouldBlock的公平策略和非公平策略的實現:
    //NonfairSync
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }


    //FairSync
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
    非公平策略調用來AQS基類的一個方法,apparentlyFirstQueuedIsExclusive,我們來看看實現:
final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }
    此函數對於如果存在第一個在等待隊列中的結點,是獨佔模式結點,則返回true。邏輯判斷也很簡單,直接獲取頭結點的後繼結點判斷是否獨佔模式即可。
    非公平策略判斷讀鎖能否插隊的時候,之所以要考慮到等待隊列,是因爲考慮到避免在等待中的寫線程會無窮盡的等待,因爲如果不設限的話,讀線程可以在讀鎖已經被獲取的情況下,無限制獲取,這樣在等待隊列中的寫線程就會被超長時間等待。
    公平策略實現很簡單,和之前的writerShouldBlock實現一樣,同樣返回hasQueuedPredecessors即可。

    至於讀鎖的tryLock實現,由於與lock實現大體一樣,只是去掉了readerShouldBlock的判斷。
    接下來分析一下ReadLock的釋放鎖實現。

(3)ReadLock釋放鎖實現
    類似地,讀鎖的unlock只是僅僅調用了releaseShared。
    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 != current.getId())
                rh = readHolds.get();
            int count = rh.count;
            if (count <= 1) {
                readHolds.remove();
                if (count <= 0)
                    throw unmatchedUnlockException();
            }
            --rh.count;
        }
        for (;;) {
            int c = getState();
            int nextc = c - SHARED_UNIT;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
    函數實現原理分爲兩大步驟:
    1、首先嚐試把當前線程的讀鎖數減去一。首先會嘗試判斷是否firstReader,如果是並且firstReaderHoldCount剛好爲1,則firstReader變爲null,如果不爲1,則減去1。如果不是firstReader線程,則嘗試cache讀取,然後判斷當前線程的讀鎖數,如果少於等於1,則會remove掉當前線程的HoldCounter,但如果發現count的值少於等於0,則要拋出IllegalMonitorStateException表示當前線程沒有獲取到讀鎖但嘗試釋放讀鎖。
    2、然後,一個自循CAS更改當前的鎖狀態數爲當前讀鎖數減去一。

    兩個步驟裏步驟1不需要自循,是因爲所做變量更改都是基於ThreadLocal的,並不會影響其它線程,步驟2由於要CAS修改鎖狀態數,因此需要自循確保成功。

總結

    到此,ReentrantReadWriteLock的框架已經解析完成。當然,如果感興趣的話,還有少部分關於鎖狀態的輔助函數,可以自行理解。
    整個讀寫鎖的實現裏,tryAcquireShared和tryReleaseShared的實現是重點,因爲要考慮多條讀線程併發修改讀寫,並且還要考慮到寫鎖的獨佔問題,需要利用到自循確保併發中的同步。另外,對於讀鎖數的記錄的實現,也針對幾種常見情況進行了優化,可見考慮是相當周到的。
發佈了36 篇原創文章 · 獲贊 4 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章