讀寫鎖. 讀鎖之間是共享的. 寫鎖是獨佔的. java
首先聲明一點: 我在分析源碼的時候, 把jdk源碼複製出來進行中文的註釋, 有時還進行編譯調試什麼的, 爲了不和jdk原生的類混淆, 我在類前面加了"My". 好比把ReentrantLock更名爲了MyReentrantLock, 在源碼分析的章節裏, 我基本不會對源碼進行修改, 因此請忽視這個"My"便可.node
unsafe在這裏是用來給TID_OFFSET賦值的.緩存
那麼TID_OFFSET是什麼? 就是tid變量在Thread類裏的偏移量. tid就是線程id.函數
下面就是獲取TID_OFFSET的源碼: (這裏我進行了一點改動, 改成了反射)源碼分析
同步器:測試
讀鎖:ui
寫鎖:線程
這是一個帶參構造器, 能夠選擇公平鎖仍是非公平鎖. 同時實例化了讀鎖和寫鎖.debug
而默認構造器是直接調用上面的帶參構造器, 採用了非公平鎖:3d
如今模擬一個場景. 兩個線程, 同時申請讀鎖, 場景的demo以下:
public class Main { static final Scanner scanner = new Scanner(System.in); private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); static volatile String cmd = ""; public static void main(String[] args) { new Thread(Main::funcA).start(); new Thread(Main::funcB).start(); while (scanner.hasNext()) { cmd = scanner.nextLine(); } } public static void funcA() { blockUntilEquals(() -> cmd, "lock a"); readLock.lock(); System.out.println("funcA獲取了讀鎖"); blockUntilEquals(() -> cmd, "unlock a"); readLock.unlock(); System.out.println("funcA釋放了讀鎖"); } public static void funcB() { blockUntilEquals(() -> cmd, "lock b"); readLock.lock(); System.out.println("funcB獲取了讀鎖"); blockUntilEquals(() -> cmd, "unlock b"); readLock.unlock(); System.out.println("funcB釋放了讀鎖"); } private static void blockUntilEquals(Supplier<String> cmdSupplier, final String expect) { while (!cmdSupplier.get().equals(expect)) quietSleep(1000); } private static void quietSleep(int mills) { try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } } }
運行上面這段代碼.
而後輸入"lock a", 而後按下回車, (不帶引號), 線程a就獲取到了讀鎖.
而後輸入"lock b", 而後按下回車, (不帶引號), 線程b就獲取到了讀鎖.
以下圖所示, 藍字爲我輸入的內容.
可見, 兩個讀鎖之間不是互斥的, 是能夠共享同一個鎖的.
接下來我們讓這兩個線程a和b 分別釋放掉讀鎖.
輸入"unlock a", 而後按下回車, (不帶括號) , 而後輸入"unlock b", 而後按下回車. 就分別釋放了兩個鎖了. 以下圖所示:
我帶着你們一塊兒調試. 請在funcA()方法裏的readLock.lock()這裏打下斷點, 而後用debug模式運行.
而後再控制檯輸入 "lock a" , 注意不帶引號, 而後按下回車:
而後就發現代碼執行到readLock.lock()處就阻塞了:
按下F7 , 進入readLock.lock()方法, 能夠看到讀鎖的lock方法的實現:
能夠看到, 調用了acquireShared方法來以共享模式申請了鎖.
acquireShared方法源代碼以下:
我們按F7(Step Into進入tryAcquireShared方法, 看看裏面的執行過程吧:
protected final int tryAcquireShared(int unused) {// 參數沒用 // 獲取當前線程的引用 Thread current = Thread.currentThread(); // 獲取鎖的狀態. c 的低 16 位值,表明寫鎖的狀態. 高16位表明讀鎖的狀態 int c = getState(); // exclusiveCount(c) 是寫鎖的state. 不等於 0,說明有線程持有寫鎖. (當前場景下確定等於0, 因此跳過這段if語句) if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 讀鎖的state int r = sharedCount(c); // 讀鎖獲取是否應該被阻塞, 其實就是根據`等待隊列`來判斷是否應該被阻塞的 ( 當前場景下沒有比當前線程等待更久的線程, 因此不會被阻塞.) if (!readerShouldBlock() && // 判斷是否會溢出 (2^16-1). (當前的r==0, 因此沒有溢出) r < MAX_COUNT && // 下面這行 CAS 是將 state 屬性的高 16 位加 1,低 16 位不變,若是成功就表明獲取到了讀鎖. (當前場景下, 沒有線程競爭, 因此確定成功.) compareAndSetState(c, c + SHARED_UNIT)) { /* ---------------------- * 進到這裏就是獲取到了讀鎖 * ----------------------*/ // r == 0 說明此線程是第一個獲取讀鎖的,或者說在它以前來的讀鎖的都走光了. (當前場景r就是等於0, 因此會執行這段if) if (r == 0) { // 記錄 firstReader 爲當前線程. firstReader = current; // 持有的讀鎖數量爲1 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 1, 表示獲取到了1個鎖. return 1; } return fullTryAcquireShared(current); }
而後點擊這個按鈕`放行`:
就發現控制檯輸出了 "funcA獲取了讀鎖" :
我們在funcB函數的這句話上也打個斷點:
而後再控制檯輸入 "lock b", 而後按下回車:
按下回車後, 就發現代碼阻塞在了剛纔的斷點上面(紅色行變爲了綠色):
而後我們開始分析線程b是若是獲取讀鎖的(記住剛纔a線程的讀鎖還沒釋放呢), 按下F7, 進入到lock()的源代碼:
再F7,
再F7, 終於到了關鍵的地方:
protected final int tryAcquireShared(int unused) {// 參數沒用 // 獲取當前線程的引用 Thread current = Thread.currentThread(); // 獲取鎖的狀態. c 的低 16 位值,表明寫鎖的狀態. 高16位表明讀鎖的狀態 int c = getState(); // exclusiveCount(c) 是寫鎖的state. 不等於 0,說明有線程持有寫鎖 (當前場景下確定等於0, 因此跳過這段if語句) if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 讀鎖的state, 因爲剛纔a線程獲取到了讀鎖, 因此這個計數器如今的值是1 int r = sharedCount(c); // 讀鎖獲取是否應該被阻塞, 其實就是根據`等待隊列`來判斷是否應該被阻塞的 ( 當前場景下沒有比當前線程等待更久的線程, 因此不會被阻塞.) if (!readerShouldBlock() && // 判斷是否會溢出 (2^16-1). (當前的r==1, 因此沒有溢出) r < MAX_COUNT && // 下面這行 CAS 是將 state 屬性的高 16 位加 1,低 16 位不變,若是成功就表明獲取到了讀鎖 (當前場景下, 沒有線程競爭, 因此確定成功.) compareAndSetState(c, c + SHARED_UNIT)) { /* ---------------------- * 進到這裏就是獲取到了讀鎖 * ----------------------*/ // 當前場景下 r == 1, 並且也不是讀鎖重入. 因此執行else語句 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { // cachedHoldCounter 用於緩存最後一個獲取讀鎖的線程 (當前場景下, cachedHoldCounter並無被賦值過, 因此是null) HoldCounter rh = cachedHoldCounter; // 當前場景下cachedHoldCounter爲空, 因此進入到這個if語句中. if (rh == null || rh.tid != getThreadId(current)) // 利用threadlocal進行建立, 並返回給cachedHoldCounter 和 rh cachedHoldCounter = rh = readHolds.get(); // 本場景下不執行這個else if, 跳過. else if (rh.count == 0) readHolds.set(rh); // 本場景下, rh剛剛被初始化, 裏面的count確定是0, 在這裏進行自增操做, 以後就變爲了1. rh.count++; } // return 1, 表示本次tryAcquireShared獲取到了1個鎖 return 1; } return fullTryAcquireShared(current); }
我們來總結一下這一段代碼都幹什麼了吧. 首先經過cas操做, 將讀鎖的state計數器加了1, (也就是變爲了2). 而後就是經過ThreadLocal.get() 方法, 在threadlocal裏建立了一個b線程的計數器, 而且把這個計數器置爲1. 而後就沒了....(代碼看起來不少的樣子, 可是實際上沒幹多少事情...)
而後將斷點放行.(今後之後就不詳細講調試過程了. 就只用語言表述了.)
回到我們的例子Main方法.在funcA函數的unlock()那一行打上斷點. 而後再控制檯輸入 "unlock a", 而後回車:
而後我們就能夠開始分析 線程a 釋放讀鎖的過程了, 按F7進入到unlock()函數內部:
再F7 :
我們先看看tryReleaseShared方法吧:
protected final boolean tryReleaseShared(int unused) {// 參數沒用 // 獲取當前線程的引用 Thread current = Thread.currentThread(); // 判斷當前線程是否是當前讀鎖中的第一個讀線程, (線程a就是第一個獲取到讀鎖的, 因此知足這個if條件.) if (firstReader == current) { assert firstReaderHoldCount > 0; // 當前場景下等於 1,因此此次解鎖後, 當前線程就不會再持有鎖了,把 firstReader 置爲 null,給後來的線程用 if (firstReaderHoldCount == 1) // 爲何不順便設置 firstReaderHoldCount = 0?由於不必,其餘線程使用的時候本身會設值 firstReader = null; // 不會執行這個else. else firstReaderHoldCount--; // 不會執行這個else語句. 跳過. } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != 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(); // state 的高 16 部分位減 1 , 低16位不動. (高16位是共享模式) // 高16位的部分, 如今是2. 在這一步減去了1, 因此執行完下面這行代碼後 nextc == 1 int nextc = c - SHARED_UNIT; // cas 更新 state的值爲nextc, (當前場景下也就是 1 了), 當前場景下沒有爭搶, cas確定成功. if (compareAndSetState(c, nextc)) // 釋放讀鎖, 對讀線程們沒有什麼影響 // 但若是是 nextc == 0,那就是 state 所有 32 位都爲 0,也就是讀鎖和寫鎖都空了 // 此時這裏返回 true 的話,實際上是幫助喚醒後繼節點中的獲取寫鎖的線程 // 當前場景下, nextc等於1.因此返回false. return nextc == 0; } }
這段代碼最終返回了false, 而後回到上一層函數. 因爲返回了false, 因此不會進入到if語句裏, 也就是不會執行doReleaseShared()方法:
而後點擊`放行`按鈕. funcA的讀鎖釋放過程就到此結束了.
回到Main方法. 我們在funcB裏的unlock()函數那一行打上斷點. 在控制檯輸入"unlock b", 而後回車.
而後調試, 一直進入到tryReleaseShared()方法. 剛纔講了tryReleaseShared釋放線程a持有的讀鎖的步驟. 我們如今看看線程b執行這段代碼會有什麼不一樣吧:
protected final boolean tryReleaseShared(int unused) {// 參數沒用 // 獲取當前線程的引用 Thread current = Thread.currentThread(); // 判斷當前線程是否是當前讀鎖中的第一個讀線程,(本場景中, 固然不是了, 並且剛纔釋放a的讀鎖的時候, firstReader被設置爲了null, 因此也不知足if. 就是說無論讀鎖a以前是否釋放了, 這裏都不會知足if條件) if (firstReader == current) { assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; // 會執行這個else語句, 而不是上面的if語句. } else { HoldCounter rh = cachedHoldCounter; // 判斷cachedHoldCounter是否是空, 當前場景下cachedHoldCounter不是空, 因此跳過這個if語句. if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); // 獲取cachedHoldCounter的計數器, 當前是 1 int count = rh.count; // 若是計數器小於等於1, 說明該釋放了.(目前知足這個if條件, 因此會執行if代碼塊) if (count <= 1) { // 這一步將 ThreadLocal中當前線程對應的計數器 remove 掉,防止內存泄漏。由於已經再也不持有讀鎖了 readHolds.remove(); // 沒鎖還要釋放? 給你拋個異常... if (count <= 0) throw unmatchedUnlockException(); } // 計數器 減 1 --rh.count; } for (; ; ) { int c = getState(); // state 的高 16 部分位減 1 , 低16位不動. (高16位是共享模式), 執行完下面這行的減1操做後, nextc就變爲0了. int nextc = c - SHARED_UNIT; // cas 設置 state if (compareAndSetState(c, nextc)) // 釋放讀鎖, 對讀線程們沒有什麼影響 // 但若是是 nextc == 0,那就是 state 所有 32 位都爲 0,也就是讀鎖和寫鎖都空了 // 此時這裏返回 true 的話,實際上是幫助喚醒後繼節點中的獲取寫鎖的線程 // 目前nextc是0, 因此會返回true. return nextc == 0; } }
最終本段代碼返回了true, 回到上層代碼, 因爲返回了true, 因此會執行if代碼塊裏的doReleaseShared()方法:
接下來, 我們看看doReleaseShared()方法都作了什麼事情吧:
因爲沒有線程進入過`等待隊列`, 因此等待隊列的head仍是null, 因此直接就break了, 什麼都沒幹.
本小節的demo, 就到此結束了.
場景以下: 線程a獲取讀鎖 -> 線程b獲取讀鎖 -> 線程a獲取寫鎖 -> 線程a釋放寫鎖 -> 線程a釋放讀鎖 -> 線程b釋放讀鎖.
1. 場景demo: (仍是那句話, 想運行我程序的, 把MyReentrantReadWriteLock 改成JDK的 ReentrantReadWriteLock 就行了. My*系列的都是我複製的JDK代碼, 而後改了個名字而已)
import java.util.Scanner; import java.util.function.Supplier; public class Main { static final Scanner scanner = new Scanner(System.in); static volatile String cmd = ""; private static MyReentrantReadWriteLock lock = new MyReentrantReadWriteLock(true); private static MyReentrantReadWriteLock.ReadLock readLock = lock.readLock(); private static MyReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); public static void main(String[] args) { new Thread(Main::funcA).start(); new Thread(Main::funcA2).start(); new Thread(Main::funcB).start(); while (scanner.hasNext()) { cmd = scanner.nextLine(); } } public static void funcA() { blockUntilEquals(() -> cmd, "lock read a"); readLock.lock(); System.out.println("funcA獲取了讀鎖"); blockUntilEquals(() -> cmd, "unlock read a"); readLock.unlock(); System.out.println("funcA釋放了讀鎖"); } public static void funcA2(){ blockUntilEquals(() -> cmd, "lock write a"); writeLock.lock(); System.out.println("funcA獲取了寫鎖"); blockUntilEquals(() -> cmd, "unlock write a"); writeLock.unlock(); System.out.println("funcA釋放了寫鎖"); } public static void funcB() { blockUntilEquals(() -> cmd, "lock read b"); readLock.lock(); System.out.println("funcB獲取了讀鎖"); blockUntilEquals(() -> cmd, "unlock read b"); readLock.unlock(); System.out.println("funcB釋放了讀鎖"); } private static void blockUntilEquals(Supplier<String> cmdSupplier, final String expect) { while (!cmdSupplier.get().equals(expect)) quietSleep(1000); } private static void quietSleep(int mills) { try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } } }
首先是a獲取讀鎖, 接下來是b獲取讀鎖. 這個場景在上小節中將讀鎖的時候已經講過了. 因此這裏一代而過.
運行上面這個場景demo, 而後按下進行輸入, 來讓a線程獲取讀鎖, 而後讓b線程獲取讀鎖:
把斷點打在writeLock.lock()方法上, 而後輸入"lock write a", 按下回車, 來讓a線程獲取寫鎖:
發現線程阻塞在了writeLock.lock()方法上. 我們開始一遍調試一遍分析代碼.
F7, 進入到了ReentrantReadWriteLock.WriteLock類裏的lock()方法:
接下來就很熟悉了, 根ReentrantLock裏的申請鎖是同一段代碼:
但還不是徹底同樣, 由於ReadWriteLock重寫了其中的tryAcquire方法:
protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); // 獲取寫鎖的重入次數, w 在本場景中等於0 int w = exclusiveCount(c); // c==0說明, 寫鎖和讀鎖都沒有. if (c != 0) { // c != 0 && w == 0: 寫鎖可用,可是有線程持有讀鎖(也多是本身持有) , 在本場景中, 會知足w==0的條件, 而進入if語句 if (w == 0 || current != getExclusiveOwnerThread()) // 返回true return false; // *********************************************************** // ---- 本場景根下面的代碼不要緊, 由於會在上一行的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; }
而後就是執行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) . 這句代碼的內部實現與ReentrantLock的代碼如出一轍(就是同一段代碼). 就再也不復述了.
執行完這句話以後, 剛剛的申請寫鎖的線程就被掛起了, 等待着讀鎖釋放完了後喚醒他.
我們知道他是經過調用AQS類裏的parkAndCheckInterrupt方法來進行掛起操做的. 我們在掛起操做的下一行打個斷點. 這樣, 到時候這個線程被喚醒後, 我們就能夠感知到了:
接下來我們在funcB函數裏的unlock()方法上打個斷點:
接下來我們釋放掉讀鎖a, 而後釋放掉讀鎖b, 而後線程就會在funcB函數裏的unlock()方法上阻塞. (釋放這兩個鎖的流程在前文中已經講過了, 因此下面簡單描述):
按F7, 進入函數內部, 一步一步調試, 最終會執行到 doReleaseShared()方法:
這裏其實就是在喚醒`等待隊列`裏的第一個寫鎖.
在這裏點擊`放行`. (點擊`放行`就是:"讓剩餘的函數自動執行完, 一直執行到下一個斷點")
就會發現跳轉到這裏了: (也就是剛纔進入等待隊列的那個申請寫鎖的線程從掛起狀態恢復到了運行狀態)
我們在此點擊`放行`按鈕, 而後這個寫鎖就申請完了:
接下來我們看看寫鎖的釋放過程. 在funcA2函數裏的unlock()方法上打上斷線, 而後再控制檯輸入"unlock write a", 並回車:
而後老規矩按F7, 進入函數內部.查看源碼:
再進入一層:
首先是嘗試釋放鎖, 若是鎖能夠徹底釋放的話, 就會激活`等待隊列`裏的第一個線程.
我們看看讀寫鎖的tryRelease方法的內部實現吧:
其實就是計數器減1, 而後若是等於0的話, 就返回true.表示鎖釋放乾淨了. 沒有重入.
而後回到上層方法, 因爲返回的是true, 因此會進入到if語句裏, 而後去判斷是否還有線程要獲取鎖. 若是有的話就用unparkSuccessor方法喚醒. 若是沒有的話就直接返回true, 而後結束:
我們在這一小節 分析一下讀鎖進入等待隊列的流程, 和讀鎖在等待隊列中被喚醒的流程.
import java.util.HashMap; import java.util.Map; import java.util.Scanner; import java.util.concurrent.locks.Lock; import java.util.function.Supplier; public class Main { static final Scanner scanner = new Scanner(System.in); static volatile String cmd = ""; private static MyReentrantReadWriteLock lock = new MyReentrantReadWriteLock(true); private static MyReentrantReadWriteLock.ReadLock readLock = lock.readLock(); private static MyReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); public static void main(String[] args) { for (Map.Entry<String, Lock> entry : new HashMap<String, Lock>() {{ put("r1", readLock); put("r2", readLock); put("r3", readLock); put("w1", writeLock); put("w2", writeLock); put("w3", writeLock); }}.entrySet()) { new Thread(() -> func(entry::getValue, entry.getKey())).start(); } // 下面這四行, 等價於上面的for循環. // new Thread(() -> func(() -> readLock, "r1")).start(); // new Thread(() -> func(() -> readLock, "r2")).start(); // new Thread(() -> func(() -> writeLock, "w1")).start(); // new Thread(() -> func(() -> writeLock, "w2")).start(); while (scanner.hasNext()) { cmd = scanner.nextLine(); } } public static void func(Supplier<Lock> myLockSupplier, String name) { String en_type = myLockSupplier.get().getClass().getSimpleName().toLowerCase().split("lock")[0]; String zn_type = (en_type.equals("read") ? "讀" : "寫"); blockUntilEquals(() -> cmd, "lock " + en_type + " " + name); myLockSupplier.get().lock(); System.out.println(name + "獲取了" + zn_type + "鎖"); blockUntilEquals(() -> cmd, "unlock " + en_type + " " + name); myLockSupplier.get().unlock(); System.out.println(name + "釋放了" + zn_type + "鎖"); } private static void blockUntilEquals(Supplier<String> cmdSupplier, final String expect) { while (!cmdSupplier.get().equals(expect)) quietSleep(1000); } private static void quietSleep(int mills) { try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } } }
運行這段代碼後, 按下面這樣進行輸入:
首先是有一個線程申請了寫鎖w2, 而後是有兩個線程分別申請了讀鎖 r1 和 r2. 等到w2被釋放的時候, r1 r2 都申請到了鎖.
(輸入的時候, 不要打錯字, 很容易打錯的.)
從新運行這段程序.先輸入"lock write w1" , 先申請寫鎖. 而後在func方法內部的`myLockSupplier.get().lock();`這一行代碼打上斷點. 而後輸入"lock read r1", 申請讀鎖.
(因爲先申請了寫鎖, 並且這個寫鎖尚未釋放. 因此這個時候申請讀鎖就意味着會進入`等待隊列`)
而後按下F7, 進入到源代碼中: (ReentrantReadWriteLock的內部類ReadLock類裏的lock()方法)
繼續按F7, 進入到方法內部, 我們就看到了acquireShared方法:
因爲剛纔我們成功申請了寫鎖, 並且還沒釋放. 因此此次讀鎖確定申請失敗.
也就是說tryAcquireShared方法嘗試獲取鎖會失敗. 失敗了就會返回-1. tryAcquireShared方法以前講過了, 就不細講了.
tryAcquireShared失敗了, 就會知足if條件. 而後就會進入到if語句中執行doAcquireShared方法. 我們繼續往下分析這個doAcquireShared :
private void doAcquireShared(int arg) { // 將節點放入`等待隊列` final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (; ; ) { final Node p = node.predecessor(); // 判斷當前是否是等待隊列中的第一個 if (p == head) { // 嘗試獲取鎖 int r = tryAcquireShared(arg); if (r >= 0) { // 把本身設置爲新的頭部, 而後看看是否須要向後續蔓延 // (也就是, 若是是Shared模式, 那麼就會把後續連續的讀鎖線程都喚醒) setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
其中主要的是setHeadAndPropagate方法. 我們進入查看源碼:
在這裏將`等待隊列`裏的第一個節點設置爲了Head節點. 而後判斷是否是下一個節點是否是共享模式的, 也就是判斷下一個節點是否是共享模式, 若是是的話, 就會執行doReleaseShared()方法. 最終會致使, 一個讀鎖獲取成功的時候, 會帶着其後續連續的讀鎖都一塊兒獲取成功.
其中的doReleaseShared()方法在前面小節已經介紹過了, 就不講了. 若是哪裏遺漏了就後續補充.