1. 程式人生 > >Java多執行緒——ReentrantReadWriteLock原始碼閱讀

Java多執行緒——ReentrantReadWriteLock原始碼閱讀

之前講了《AQS原始碼閱讀》《ReentrantLock原始碼閱讀》,本次將延續閱讀下ReentrantReadWriteLock,建議沒看過之前兩篇文章的,先大概瞭解下,有些內容會基於之前的基礎上閱讀。
這個並不是ReentrantLock簡單的升級,而是落地場景的優化,我們來詳細瞭解下吧。

背景

JUC包裡面已經有一個ReentrantLock了,為何還需要一個ReentrantReadWriteLock呢?看看頭註解找點線索。

它是ReadWriteLock介面的實現。那看看這個介面怎麼說

在實際場景中,一般來說,讀資料遠比寫資料要多。如果我們還是用獨佔鎖去鎖執行緒避免執行緒不安全的話,是非常低效的,而且同時也會失去它的併發性。多執行緒也沒有意義了。所以ReadWriteLock就是解決這個問題所存在的。
看回ReentrantReadWriteLock的頭註解。


ReentrantReadWriteLock依然有公平鎖/非公平鎖的功能,與ReentrantLock不同在於,前者內部維護了讀鎖和寫鎖,在公平/非公平模式下,他們會一起去競爭這個鎖資源。

上圖是兩條ReentrantReadWriteLock最核心的規則。

  1. 申請讀鎖。當沒有其他寫鎖佔有,或者讀鎖在佇列中排隊時間最長的,才能成功
  2. 申請寫鎖。當沒有其他執行緒佔有讀/寫鎖的情況下,才能成功

又以上兩條規則可以推匯出,

  1. 寫鎖比讀鎖要高階
  2. 有讀鎖佔用可以繼續申請讀鎖,但其他執行緒不能申請寫鎖
  3. 有寫鎖佔用其他執行緒讀寫都不能申請

所以扣ReadWriteLock介面的說明,可以讓讀併發,寫獨佔,提高了程式的併發性。

ReentrantReadWriteLock構成

看下ReentrantReadWriteLock的file struture

之前看過ReentrantLock原始碼的同學肯定很熟悉這個結構,看起來相同的都是Sync同步器(AQS的子類),以及它的兩個公平/非公平子類。
不同的是它還多了ReadLock內部類和WriteLock內部類,以及讀寫對應的成員變數和方法。並且少了lock()、unlock()等方法,而是把加鎖解鎖的功能下方給這兩個子類,符合ReadWriteLock介面的定義。

Sync內部類

雖然ReentrantReadWriteLock和ReentrantLock都有Sync,但其實Sync方法已經很大不同了,看下Sync的結構


對比之前ReentrantLock的Sync,最大不同在於它多了**shared()方法,用於共享鎖的獲取與釋放。
另外tryReadLock()、tryWriteLock()是給WriteLock和ReadLock內部類使用的。

tryAcquire() 獨佔鎖(寫鎖)申請


上文介紹重入鎖說到state代表的是重入的次數,在讀寫鎖的語義下,state代表的讀/寫佔有(重入)的次數。c為state,w為獨佔重入次數。
當有執行緒佔用鎖時(c!=0),如果沒有寫鎖(w==0)或者獨佔執行緒不是當前執行緒,返回false獲取失敗。鎖的重入總數超過上限會丟擲異常。
這裡很容易看出來,如果有鎖佔用的時候,如果只是讀鎖,依然可以申請成功。這就是讀鎖的鎖升級
當沒有執行緒佔用的時候,執行writerShouldBlock()判斷是否需要阻塞執行緒(子類實現自己的條件),不需要則CAS state值,返回成功。

tryAquireShared() 共享鎖(讀鎖)申請


讀鎖申請比寫鎖申請要複雜,有比較多沒接觸過的成員變數,判斷的語句也比較多。
先看看成員變數,從他們各自的變數註解可知

  • firstReader,是第一個獲取讀鎖的執行緒
  • firstReaderHoldCount,是firstReader的計數器。
  • cachedHoldCounter,最近一個成功獲取讀鎖的執行緒持有數計數器。
  • readHolds,當前執行緒重入讀鎖次數。ThreadLocal

先判斷是否有寫鎖佔有,如果寫鎖不是當前執行緒,獲取讀鎖失敗,退出方法。
注意如果寫鎖是當前執行緒是可以獲取讀鎖的,因為寫鎖是獨佔的,這種情況下是不會有資料與其他執行緒共享的問題。
滿足子類條件,也不超過總數,CAS也成功的情況下,
如果沒有讀鎖,則設firstReader為當前執行緒,firstReaderHoldCount為1;
如果有讀鎖,並且也是當前執行緒申請獲取,firstReaderHoldCount自增1;
如果有讀鎖,不是當前執行緒申請,取上一個成功的快取計數器,如果這個計數器不是當前執行緒的,則設為當前的計數器,並且自增,返回成功。(其實就是把快取計數器置換為當前執行緒的計數器)
最後不滿足條件或者CAS失敗,執行fullTryAcquireShared(current)返回。
至於這些資料算來幹嘛,等後面看看release()怎麼用。

其實這個方法就是用for迴圈輪詢解決CAS丟失和重入失敗的問題,具體程式碼不細過了,有興趣可以自己找原始碼看看。

tryRelease() 獨佔鎖(寫鎖)釋放


這裡又有Condition的蹤跡了,大概可以才行到Condition時控制鎖的行為的,取消喚醒等操作。
另外鎖會同時釋放讀鎖和寫鎖。
這個方法比較好理解的,只要是當前執行緒操作下,持有重入數減去釋放數為0就可以釋放了,否則失敗。

tryReleaseShared() 共享鎖(讀鎖)釋放


釋放讀鎖,對正在讀的執行緒不會有什麼影響,但可以讓等待的寫執行緒去開始獲取寫鎖。
剩餘的內容就是對tryAquireShared()計算的count數值進行釋放(自減),如果最終自減為0則釋放讀鎖成功。

WriteLock、ReadLock內部類

前面說到ReentrantReadWriteLock的lock()、unlock()操作是分配到Write/ReadLock裡面執行的。
他們都是Lock介面的實現,所以其實最像ReentrantLock應該是這個兩個內部類。而且大體上也沒什麼差異,也是用Sync的內部類。
WriteLock、ReadLock最大的不同就是WriteLock用的獨佔模式的方法,ReadLock用的是共享模式的方法。
具體的程式碼實現基本就是上面說明的組成,下面介紹下ReentranReadWriteLock的使用。
ReentrantLock的時候比較簡單,宣告一個變數,呼叫lock()方法即可。

    ReentrantLock rl = new ReentrantLock();
    rl.lock();
    rl.unlock();

但ReentranReadWriteLock並不是Lock介面的實現,所以沒有這些方法。
有的只是writeLock()、readLock(),要先呼叫這個方法獲取應對的鎖物件,再呼叫lock()。

     ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     rwl.readLock().lock();
     rwl.readLock().unlock();
     rwl.writeLock().lock();
     rwl.writeLock().unlock();

總結

回顧下要點

  1. 讀寫鎖ReentrantReadWriteLock,是基於多讀少寫的實際場景,提高併發性
  2. 讀寫鎖的Sync添加了共享模式的方法
  3. 讀寫鎖內建了兩個物件readLock、writeLock,用於實際的加鎖解鎖
  4. 寫鎖是獨佔的,不允許其他鎖的申請
  5. 讀鎖可以併發重複申請,當有寫鎖的時候,會發生鎖升級

特別地,在此祝福8月27日生日的她。

更多技術文章、精彩乾貨,請關注
部落格:zackku.com
微信公眾號:Zack說碼