Java多線程進階(十)—— J.U.C之locks框架:基於AQS的讀寫鎖(5)

12.jpeg

本文首發於一世流雲的專欄: https://segmentfault.com/blog...

1、本章概述

AQS系列的前四個章節,已經分析了AQS的原理,本章將會從ReentrantReadWriteLock出發,給出其內部利用AQS框架的實現原理。segmentfault

ReentrantReadWriteLock(如下簡稱RRW),也就是讀寫鎖,是一個比較特殊的同步器,特殊之處在於其對同步狀態State的定義與ReentrantLock、CountDownLatch都很不一樣。經過RRW的分析,咱們能夠更深入的瞭解AQS框架的設計思想,以及對什麼是資源?如何定義資源是否能夠被訪問?這一命題有更深入的理解。性能優化

關於ReentrantReadWriteLock的使用和說明,讀者能夠參考: Java多線程進階(四)—— juc-locks鎖框架:ReentrantReadWriteLock

2、本章示例

和以前的章節同樣,本章也經過示例來分析RRW的源碼。多線程

假設如今有4個線程,ThreadA、ThreadB、ThreadC、ThreadD。
ThreadA、ThreadB、ThreadD爲讀線程,ThreadC爲寫線程:

初始時,構造RRM對象:
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();框架

//ThreadA調用讀鎖的lock()方法

//ThreadB調用讀鎖的lock()方法

//ThreadC調用寫鎖的lock()方法

//ThreadD調用讀鎖的lock()方法

3、RRW的公平策略原理

1. RRW對象的建立

和ReentrantLock相似,ReentrantReadWriteLock的構造器能夠選擇公平/非公平策略(默認爲非公平策略),RRW內部的FairSyncNonfairSync是AQS的兩個子類,分別表明了實現公平策略和非公平策略的同步器:
clipboard.png性能

ReentrantReadWriteLock提供了方法,分別獲取讀鎖/寫鎖:
clipboard.png優化

ReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock其實就是兩個實現了Lock接口的內部類:
clipboard.pngui

2. ThreadA調用讀鎖的lock()方法

讀鎖實際上是一種共享鎖,實現了AQS的共享功能API,能夠看到讀鎖的內部就是調用了AQS的acquireShared方法,該方法前面幾章咱們已經見過太屢次了:
clipboard.pngspa

關鍵來看下ReentrantReadWriteLock是如何實現tryAcquireShared方法的:
讀鎖獲取成功的條件以下:線程

  1. 寫鎖沒有被其它線程佔用(可被當前線程佔用,這種狀況屬於鎖降級)
  2. 等待隊列中的隊首沒有其它線程(公平策略)
  3. 讀鎖重入次數沒有達到最大值
  4. CAS操做修改同步狀態值State成功

clipboard.png

若是CAS操做失敗,會調用fullTryAcquireShared方法,自旋修改State值:
clipboard.png設計

ThreadA調用完lock方法後,等待隊列結構以下:
clipboard.png

此時:
寫鎖數量:0
讀鎖數量:1

3. ThreadB調用讀鎖的lock()方法

因爲讀鎖是共享鎖,且此時寫鎖未被佔用,因此此時ThreadB也能夠拿到讀鎖:
ThreadB調用完lock方法後,等待隊列結構以下:
clipboard.png

此時:
寫鎖數量:0
讀鎖數量:2

4. ThreadC調用寫鎖的lock()方法

寫鎖實際上是一種獨佔鎖,實現了AQS的獨佔功能API,能夠看到寫鎖的內部就是調用了AQS的acquire方法,該方法前面幾章咱們已經見過太屢次了:
clipboard.png

關鍵來看下ReentrantReadWriteLock是如何實現tryAcquire方法的,並無什麼特別,就是區分了兩種狀況:

  1. 當前線程已經持有寫鎖
  2. 寫鎖未被佔用

clipboard.png

ThreadC調用完lock方法後,因爲存在使用中的讀鎖,因此會調用acquireQueued並被加入等待隊列,這個過程就是獨佔鎖的請求過程(AQS[二]),等待隊列結構以下:
clipboard.png

此時:
寫鎖數量:0
讀鎖數量:2

5. ThreadD調用讀鎖的lock()方法

這個過程和ThreadA和ThreadB幾乎同樣,讀鎖是共享鎖,能夠重複獲取,可是有一點區別:
因爲等待隊列中已經有其它線程(ThreadC)排在當前線程前,因此readerShouldblock方法會返回true,這是公平策略的含義。

clipboard.png

雖然獲取失敗了,可是後續調用fullTryAcquireShared方法,自旋修改State值,正常狀況下最終修改爲功,表明獲取到讀鎖:
clipboard.png

最終等待隊列結構以下:
clipboard.png

此時:
寫鎖數量:0
讀鎖數量:3

6. ThreadA釋放讀鎖

內部就是調用了AQS的releaseShared方法,該方法前面幾章咱們已經見過太屢次了:
clipboard.png
clipboard.png

關鍵來看下ReentrantReadWriteLock是如何實現tryReleaseShared方法的,沒什麼特別的,就是將讀鎖數量減1:
clipboard.png

注意:
HoldCounter是個內部類,經過與 ThreadLocal結合使用保存每一個線程的持有讀鎖數量,實際上是一種優化手段。
clipboard.png
此時:
寫鎖數量:0
讀鎖數量:2

7. ThreadB釋放讀鎖

和ThreadA的釋放徹底同樣,此時:

寫鎖數量:0
讀鎖數量:1

8. ThreadD釋放讀鎖

和ThreadA的釋放幾乎同樣,不一樣的是此時讀鎖數量爲0,tryReleaseShared方法返回true:
clipboard.png

此時:
寫鎖數量:0
讀鎖數量:0

clipboard.png

所以,會繼續調用doReleaseShared方法,doReleaseShared方法以前在講AQS[四]時已經闡述過了,就是一個自旋操做:
clipboard.png

該操做會將ThreadC喚醒:
clipboard.png

9. ThreadC從原阻塞處繼續向下執行

ThreadC從原阻塞處被喚醒後,進入下一次自旋操做,而後調用tryAcquire方法獲取寫鎖成功,並從隊列中移除:
clipboard.png

等待隊列最終狀態:
clipboard.png

此時:
寫鎖數量:1
讀鎖數量:0

10. ThreadC釋放寫鎖

其實就是獨佔鎖的釋放,在AQS[二]中,已經闡述過了,再也不贅述。

補充一點:若是頭結點後面還有等待的共享結點,會以傳播的方式依次喚醒,這個過程就是共享結點的喚醒過程,並沒有區別。

clipboard.png

4、總結

本章經過ReentrantReadWriteLock的公平策略,分析了RRW的源碼,非公平策略分析方法也是同樣的,非公平和公平的最大區別在於寫鎖的獲取上:
clipboard.png

在非公平策略中,寫鎖的獲取永遠不須要排隊,這其實時性能優化的考慮,由於大多數狀況寫鎖涉及的操做時間耗時要遠大於讀鎖,頻次遠低於讀鎖,這樣能夠防止寫線程一直處於飢餓狀態。

關於ReentrantReadWriteLock,最後有兩點規律須要注意:

  1. 當RRW的等待隊列隊首結點是共享結點,說明當前寫鎖被佔用,當寫鎖釋放時,會以傳播的方式喚醒頭結點以後緊鄰的各個共享結點。
  2. 當RRW的等待隊列隊首結點是獨佔結點,說明當前讀鎖被使用,當讀鎖釋放歸零後,會喚醒隊首的獨佔結點。

ReentrantReadWriteLock的特殊之處其實就是用一個int值表示兩種不一樣的狀態(低16位表示寫鎖的重入次數,高16位表示讀鎖的使用次數),並經過兩個內部類同時實現了AQS的兩套API,核心部分與共享/獨佔鎖並沒有什麼區別。

相關文章
相關標籤/搜索