ReentrantReadWriteLock源碼解析

上回說到ReentrantLock,今天來談談讀寫鎖(ReentrantLock)和其具體實現ReentrantReadWriteLock。看這篇文章前,強烈建議你回到先讀懂ReentrantLock,因爲ReentrantReadWriteLock其實是在ReentrantLock的基礎上實現的,可以參考我之前的博客ReentrantLock源碼解析

既然有了鎖,爲什麼還需要讀寫鎖?我們來想象下這個場景。你們小區樓下有個公告欄,有時候有人會寫個招租,有時候有人會寫個尋物啓事…… 當然一個人正在改公告欄的時候,另外一個人就不能同時改了,這裏就相當於有了一把無形的鎖,我改的時候就把廣告欄“鎖住”,改完再“解鎖”,當然別人鎖住了之後我也改不了。說完了“寫”再說“讀”,一個人在讀公告欄的時候,別人就不能去寫了,這樣不禮貌,這裏也相當於讀的人用一把“鎖”把公告欄給鎖了。

如果這裏讀者用的鎖和寫者用的鎖是一樣的,那麼這把鎖不緊不然別人寫了,也不讓別人讀了,相當於一個人在看公告欄,別人就不能看了,這明顯不合理啊。 所以要把讀和寫用的鎖區分開來,所有讀的人共享一把鎖,寫的人獨享鎖。放到公告欄的例子上,改公告的時候同時只有一個人可以看,但讀的時候所有人可以同時讀,這樣就可以把“公告欄”這個資源的利用率最大化。

看到這裏,你應該已經理解了什麼叫做“讀寫鎖”,接下來我們直接看下jdk中ReentrantReadWriteLock的實現,再次建議先閱讀ReentrantLock的具體實現。
在這裏插入圖片描述

從類結構圖看,貌似它比ReentrantLock更復雜寫,多兩個內部類 ReadLockWriteLock,看着Lock提供的api完全一樣,看來得從具體實現上來看其二者有什麼樣的差異了。

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

從ReentrantReadWriteLock的構造方法可以看出,它也支持公平鎖和非公平鎖,當然默認也是非公平鎖。和ReentrantLock一樣,加鎖和解鎖的實現邏輯都是在 Sync 裏,所以我們重點看下Sync的實現,代碼太多這裏就不貼完整代碼了,建議讀者自行打開代碼。
在這裏插入圖片描述

Sync

從Sync的類結構圖來看,它還是相當複雜的,別急讓我們來捋一捋,我們先從WriteLock看起(看起來會比較熟悉),看下他的lock和release的具體實現。

        @ReservedStackAccess
        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();   // 1
            int c = getState();    // 2
            if (c != 0) {    // 3 
                int w = exclusiveCount(c);   // 4
                if (w == 0 || current != getExclusiveOwnerThread())  // 5
                    return false;
                if (w == MAX_COUNT)   //6.  MAX_COUNT = 65535
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))  // 7
                return false;
            setExclusiveOwnerThread(current);  // 8
            return true;
        }    

如果你看過ReentrantLock的話,相信這段代碼你已經完全能看懂了。這裏我再大概說下這段代碼的流程

  1. 獲取到當前線程。
  2. 獲取到鎖對象的state值,state是保存了鎖的狀態。
  3. 如果state不爲0,說明已經有線程加過鎖了,這時候需要額外判斷下,跳到4。 如果state爲0,直接跳到 7。
  4. 獲取到當前加寫鎖的次數,這裏獲取的是state的低16位。
  5. c已經不爲0了,如果w不爲0說明有線程加了寫鎖,如果加了寫鎖的線程也不是當前線程的,加鎖就失敗了。
  6. 這裏需要額外判斷下鎖重入的次數,如果已經到65535就不能再加鎖了,後續會解釋爲什麼是65535。
  7. 執行CAS操作更改鎖狀態 state。
  8. 到這裏說明加寫鎖已經成功了,把當前鎖的持有者記錄下來。
        @ReservedStackAccess
        final boolean tryReadLock() {
            Thread current = Thread.currentThread();   // 1
            for (;;) {
                int c = getState();  // 2
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)  // 3 
                    return false;
                int r = sharedCount(c);  // 4
                if (r == MAX_COUNT)   // 5
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {  // 6
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null ||
                            rh.tid != LockSupport.getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

讀鎖的加鎖代碼就完全不一樣了,第一眼看到的不同就是這裏有個大大的無限循環,我們還是來看下讀鎖的加鎖過程。

  1. 獲取當前線程。
  2. 獲取鎖的state狀態值。
  3. 如果寫鎖的加鎖次數不是0切寫鎖持有者不是當前線程,加讀鎖失敗。
  4. 獲取讀鎖的加鎖次數,sharedCount©獲取的是state的高16位。
  5. 如果讀鎖加鎖次數達到65535,拋Error,和寫鎖一樣,只能加65535次。
  6. 執行到這,說明可以加鎖,使用CAS更新state成功後這裏就開始記錄一些讀鎖的狀態信息,注意這裏state增加值不是1,而是SHARED_UNIT(65536)。

看完readLock和writeLock的加鎖方式就可以大體理解ReentrantReadWriteLock的實現了,原來它只是把ReentrantLock中的state分成兩部分來用,高16位記錄讀鎖狀態,低16位記錄寫鎖狀態,如下圖。
在這裏插入圖片描述
這也是爲什麼上文中加鎖最大次數是65535的原因了,這也是而是SHARED_UNIT的值爲65536的原因。

理解了加鎖的代碼,解鎖部分也就好理解了,本質上是把加鎖的代碼反向執行下,代碼如下。

        @ReservedStackAccess
        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;
        }
        
        @ReservedStackAccess
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null ||
                    rh.tid != LockSupport.getThreadId(current))
                    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))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

Sync中還有一個ThreadLocalHoldCounter類,這個類的作用其實是記錄每個線程對讀鎖的加鎖測試,見名知意線程級的統計,代碼也很簡單,這裏就不再貼了。
Sync中除了上文說到的幾個加解鎖的API,其餘一些API就是獲取Sync對象中各個狀態的API,沒什麼好說的。

FairSync & NonfairSync

說完了抽象類Sync,我們來說下它的兩個具體實現 FairSyncNonfairSync。 這兩個實現類非常非常簡單,只是重寫了 writerShouldBlock()readerShouldBlock() 方法而已,如果你已經知道什麼是公平和非公平了,這地方也就很好理解了。

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() { 
        // 寫鎖可以始終不被等待隊列裏的線程阻塞,只要當前鎖是未鎖定狀態就可以加鎖 
            return false;
        }
        final boolean readerShouldBlock() {  
        //這個方法判斷隊列的head.next是否正在等待寫鎖,這個方法確保讀鎖不應該讓寫鎖始終等待,即便是非公平的,但寫鎖有更高的優先級,獲取讀鎖還是得排隊。
            return apparentlyFirstQueuedIsExclusive();
        }
    }

    // 公平鎖就很好理解了,只要等待隊列不爲空,就得去排隊  
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

ReadLock & WriteLock

在這裏插入圖片描述
在這裏插入圖片描述
其實看完Sync裏的邏輯,基本上ReadLock和WriteLock的實現邏輯我們已經知道了。ReadLock和WriteLock只是向用戶提供裏有些功能抽象(實現了Lock中的方法),封裝好了具體的實現,其實具體邏輯還是在Sync中實現。

從類繼承關係來看,二者也只是簡單

結論

瞭解完ReentrantReadWriteLock的實現後你就會發現,它其實和ReentrantLock一樣,之前把ReentrantLock中的state切分成兩部分用,高16位作爲讀鎖的state,低16位作爲寫鎖。如果把ReadLock和WriteLock拉出來單獨看的話,二者都是一個ReentrantLock,只是不能像ReentrantLock那樣重入那麼多次而已。

ReentrantReadWriteLock的出現大幅提升了多讀少寫場景下的性能問題,但它依舊有自己的缺點,就是它可能會導致寫飢餓。還是拿小區公告欄的例子,如果任意時刻都有人在看公告欄,你也不好打斷人家所以你公告更新不了啊,所以想更新的人就得一直等着。
關注我,下次和大家一起看下 StampedLock 是如何解決飢餓問題的。

發佈了233 篇原創文章 · 獲贊 563 · 訪問量 50萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章