1. 程式人生 > >ReentrantReadWriteLock 原始碼分析以及 AQS 共享鎖 (二)

ReentrantReadWriteLock 原始碼分析以及 AQS 共享鎖 (二)

## 前言 上一篇講解了 AQS 的獨佔鎖部分(參看:[ReentrantLock 原始碼分析以及 AQS (一)](https://mp.weixin.qq.com/s/dDjbR76U5C696CXAOAvpng)),這一篇將介紹 AQS 的共享鎖,以及基於共享鎖實現讀寫鎖分離的 ReentrantReadWriteLock。(若是遇到之前講過的方法,將不再贅述) **先思考一下,為什麼我們用讀寫鎖分離?** 我們知道 ReentrantLock 用的是獨佔鎖,不管執行緒是讀還是寫狀態,都會阻塞,這無疑會降低併發量。 但是,我們知道多個執行緒同時去讀資料的時候,並不會產生執行緒安全的問題,因為它們互不干擾。那麼為什麼不設計一種方案,讓所有的讀執行緒可以共享,一起同時讀資料呢,只需要阻塞寫的執行緒就可以了。提高併發的同時,也不會產生資料不一致的現象。 同樣的,如果有執行緒在寫資料,那麼也會阻塞其它讀執行緒(同樣阻塞其它寫執行緒),資料寫完之後才可以讀資料,這樣保證讀到的資料都是最新的。 因此,我們可以用讀、寫兩把鎖,分別控制資料的讀和寫。實現讀讀共享、讀寫互斥,寫寫互斥。這也是 ReentrantReadWriteLock 讀寫分離鎖的由來。它非常適合用在讀多寫少的場景。 ## ReentrantReadWriteLock 它和 ReentrantLock 一樣,也是一個可重入的鎖,並基於 AQS 共享鎖實現了讀寫分離。其內部結構也大同小異,支援公平鎖和非公平鎖。我們看下它的建構函式, ``` public ReentrantReadWriteLock() { //預設非公平 this(false); } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } ``` 它定義了兩個內部類來表示讀鎖和寫鎖,並且都通過內部類 Sync 來實現加鎖,釋放鎖等功能。 ``` 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 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; } ... } abstract static class Sync extends AbstractQueuedSynchronizer { } ``` 我們再看下公平鎖和非公平鎖,其中有兩個比較重要的方法,用來判斷讀鎖和寫鎖是否應該被阻塞,後面加鎖的時候會用到(其實,實際情況是否真的應該阻塞,還需要斟酌,後面會說)。 ``` static final class FairSync extends Sync { private static final long serialVersionUID = -2274990926593161451L; //公平鎖的讀和寫都需要判斷,在它前面是否已經有執行緒在等待。 //有的話,當前執行緒就需要阻塞,這也體現了公平性。 final boolean writerShouldBlock() { return hasQueuedPredecessors(); } final boolean readerShouldBlock() { return hasQueuedPredecessors(); } } static final class NonfairSync extends Sync { private static final long serialVersionUID = -8159625535654395037L; //非公平鎖,寫的時候不需要阻塞,直接返回false final boolean writerShouldBlock() { return false; // writers can always barge } final boolean readerShouldBlock() { //為了避免寫執行緒飢餓,需要判斷同步佇列中第一個排隊的(head.next)是否是獨佔鎖(寫執行緒) //如果是的話,當前讀執行緒就需要阻塞,這是 AQS 中的方法 return apparentlyFirstQueuedIsExclusive(); } } final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null; } ``` **思考:** 我們知道 ReentrantLock 的同步狀態和重入次數,是直接用 state 值來表示的。那麼,現在我需要讀和寫兩把鎖,怎麼才能用一個 int 型別的值來表示兩把鎖的狀態呢?並且,鎖是可重入的,重入的次數怎麼記錄呢? 別急,下面一個一個說。 ### 怎麼用一個 state 值表示讀、寫兩把鎖? ![](https://img2020.cnblogs.com/other/1714084/202003/1714084-20200317204510064-1840955138.jpg) state 是一個 32 位的 int 值,讀寫鎖中,把它一分為二,高 16 位用來表示讀狀態,其值代表讀鎖的執行緒數,如圖中為 3 個,低 16位表示寫狀態,其值代表寫鎖的重入次數(因為是獨佔鎖)。 這樣,就可以分別計算讀鎖和寫鎖的個數了。其相關的屬性和方法定義在 Sync 類中。 ``` static final int SHARED_SHIFT = 16; //表明讀鎖每增加一個,state的實際值增加 2^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; //持有讀鎖的執行緒個數,引數如的 c 代表 state值 //state 的32位二進位制位,無符號右移 16位之後,其實就是高16位的值 static int sharedCount(int c) { return c >>> SHARED_SHIFT; } //寫鎖數量,即寫鎖的重入次數 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } ``` 讀鎖的個數計算比較簡單,直接無符號右移 16 位即可。我們看下寫鎖的重入次數是怎麼計算的。先看下 EXCLUSIVE_MASK 這個值,是 (1 << 16) - 1,我們用二進位制表示計算過程為: ``` // 1的二進位制 0000 0000 0000 0000 0000 0000 0000 0001 // 1左移 16位 0000 0000 0000 0001 0000 0000 0000 0000 //再減 1 0000 0000 0000 0000 1111 1111 1111 1111 //任何一個 32位二進位制數 c,和以上值做 “與” 運算都為它本身 c 的低 16 位值 //這個不用解釋了吧,這個不會的話,需要好好補充一下基礎知識了。。。 ``` ### 鎖的重入次數是怎麼計算的? 寫鎖比較簡單,直接用計算出來的低16位值就可以代表寫鎖的重入次數。 讀鎖,就比較複雜了,因為高16位只能表示持有共享鎖的執行緒個數,實在是分身乏術啊。所以,在 Sync 內部,維護了一個類,用來表示每個執行緒重入的次數, ``` static final class HoldCounter { int count = 0; // Use id, not reference, to avoid garbage retention final long tid = getThreadId(Thread.currentThread()); } ``` 這裡邊定義了一個計數器來表示重入次數,tid 來表示當前的執行緒 id 。但是,這樣還不夠,我們需要把 HoldCounter 和 執行緒繫結,這樣才可以區分出來每個執行緒分別持有的鎖個數(重入次數),這就需要用到 ThreadLocal 了。 ``` static final class ThreadLocalHoldCounter extends Thr