天天看點

全網最詳細的ReentrantReadWriteLock源碼剖析(萬字長文)

全網最詳細的ReentrantReadWriteLock源碼剖析(萬字長文)

萬字長文解析JUC讀寫鎖:ReentrantReadWriteLock深度剖析

碎碎念) 花了兩天時間,終于把<code>ReentrantReadWriteLock</code>(讀寫鎖)解析做完了。之前鑽研過AQS(<code>AbstractQueuedSynchronizer</code>)的源碼,發現弄懂讀寫鎖也沒有想象中那麼困難。而且閱讀完<code>ReentrantReadWriteLock</code>的源碼,正好可以和AQS的源碼串起來了解,相輔相成 AQS的連結貼在下方👇👇👇 全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(一)AQS基礎 全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的擷取和釋放 全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(三)條件變量

<code>ReentrantReadWriteLock</code>是一個可重入讀寫鎖,内部提供了讀鎖和寫鎖的單獨實作。其中讀鎖用于隻讀操作,可被多個線程共享;寫鎖用于寫操作,隻能互斥通路

<code>ReentrantReadWriteLock</code>尤其适合讀多寫少的應用場景

讀多寫少: 在一些業務場景中,大部分隻是讀資料,寫資料很少,如果這種場景下依然使用獨占鎖(如<code>synchronized</code>),會大大降低性能。因為獨占鎖會使得本該并行執行****的讀操作,變成了串行執行

<code>ReentrantReadWriteLock</code>實作了<code>ReadWriteLock</code>接口,該接口隻有兩個方法,分别用于傳回讀鎖和寫鎖,這兩個鎖都是<code>Lock</code>對象。該接口源碼如下:

<code>ReentrantReadWriteLock</code>有兩個域,分别存放讀鎖和寫鎖:

ReentrantReadWriteLock的核心原理主要在于兩點:

内部類<code>Sync</code>:實作了的AQS大部分方法。<code>Sync</code>類有兩個子類<code>FairSync</code>和<code>NonfairSync</code>,分别實作了公平讀寫鎖和非公平讀寫鎖。<code>Sync</code>類及其子類的源碼解析會在後面給出

内部類<code>ReadLock</code>和<code>WriteLock</code>:分别是讀鎖和寫鎖的具體實作,它們都和<code>ReentrantLock</code>一樣實作了<code>Lock</code>接口,是以實作的手段也和<code>ReentrantLock</code>一樣,都是委托給内部的<code>Sync</code>類對象來實作,對應的源碼解析也會在後面給出

說什麼<code>Sync</code>類、<code>ReadLock</code>、<code>WriteLock</code>類啥的都太抽象,不如一張圖來得實在!<code>ReentrantReadWriteLock</code>和這些内部類的繼承、聚合關系如下圖所示:

全網最詳細的ReentrantReadWriteLock源碼剖析(萬字長文)

作者:酒冽        出處:https://www.cnblogs.com/frankiedyz/p/15655865.html

版權:本文版權歸作者和部落格園共有

轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接配接;否則必究法律責任

讀鎖和寫鎖之間是互斥關系:當有線程持有讀鎖時,寫鎖不能獲得;當有其他線程持有寫鎖時,讀鎖不能獲得

讀鎖和讀鎖之間是共享關系

寫鎖和寫鎖之間是互斥關系

<code>ReentrantReadWriteLock</code>在<code>ReadWriteLock</code>接口之上,添加了可重入的特性,且讀鎖和寫鎖都支援可重入。可重入的含義是:

如果一個線程擷取了讀鎖,那麼它可以再次擷取讀鎖(但直接擷取寫鎖會失敗,原因見下方的“鎖的升降級”)

如果一個線程擷取了寫鎖,那麼它可以再次擷取寫鎖或讀鎖

<code>ReentrantReadWriteLock</code>不支援鎖更新,即同一個線程擷取讀鎖後,直接申請寫鎖是不能擷取成功的。測試代碼如下:

運作到第6行會因為擷取失敗而被阻塞,導緻Test1發生死鎖。指令行輸出如下:

<code>ReentrantReadWriteLock</code>支援鎖降級,即同一個線程擷取寫鎖後,直接申請讀鎖是可以直接成功的。測試代碼如下:

該程式不會産生死鎖。結果輸出如下:

<code>ReentrantReadWriteLock</code>不支援鎖更新,因為可能有其他線程同時持有讀鎖,而讀寫鎖之間是互斥的,是以更新為寫鎖存在沖突

<code>ReentrantReadWriteLock</code>支援鎖降級,因為如果該線程持有寫鎖時,一定沒有其他線程持有讀鎖或寫鎖,是以降級為讀鎖不存在沖突

<code>ReentrantReadWriteLock</code>支援公平模式和非公平模式擷取鎖。從性能上來看,非公平模式更好

二者的規則如下:

公平鎖:無論是讀線程還是寫線程,在申請鎖時都會檢查是否有其他線程在同步隊列中等待。如果有,則讓步

非公平鎖:如果是讀線程,在申請鎖時會判斷是否有寫線程在同步隊列中等待。如果有,則讓步。不過這是為了防止寫線程餓死,與公平政策無關;如果是寫線程,則直接競争鎖資源,不會關心有無别的線程正在等待

<code>Sync</code>類是一個抽象類,有兩個具體子類<code>NonfairSync</code>和<code>FairSync</code>,分别對應非公平讀寫鎖、公平讀寫鎖。<code>Sync</code>類的主要作用就是為這兩個子類提供絕絕絕大部分的方法實作

隻定義了兩個抽象方法<code>writerShouldBlock</code>和<code>readerShouldBlocker</code>交給兩個子類去實作

<code>Sync</code>類利用AQS單個<code>state</code>字段,來同時表示讀狀态和寫狀态,源碼如下:

根據上面源碼可以看出:

<code>SHARED_SHIFT</code>表示AQS中的<code>state</code>(int型,32位)的高16位,作為讀狀态,低16位作為寫狀态

<code>SHARED_UNIT</code>二級制為2^16,讀鎖加1,<code>state</code>加<code>SHARED_UNIT</code>

<code>MAX_COUNT</code>就是寫或讀資源的最大數量,為2^16-1

使用<code>sharedCount</code>方法擷取讀狀态,使用<code>exclusiveCount</code>方法擷取擷取寫狀态

<code>state</code>劃分為讀、寫狀态的示意圖(圖來自網絡)如下,其中讀鎖持有1個,寫鎖持有3個:

全網最詳細的ReentrantReadWriteLock源碼剖析(萬字長文)

<code>firstReader</code>記錄首個獲得讀鎖的線程;<code>firstReaderHoldCount</code>記錄<code>firstReader</code>持有的讀鎖數

<code>Sync</code>類定義了一個線程局部變量<code>readHolds</code>,用于儲存目前線程重入讀鎖的次數。如果該線程的讀鎖數減為0,則将該變量從線程局部域中移除。相關源碼如下:

由于<code>readHolds</code>變量是線程局部變量(繼承<code>ThreadLocal</code>類),每個線程都會儲存一份副本,不同線程調用其get方法傳回的HoldCounter對象不同

<code>readHolds</code>中的<code>HoldCounter</code>變量儲存了每個讀線程的重入次數,即其持有的讀鎖數量。這麼做的目的是便于線程釋放讀鎖時進行合法性判斷:線程在不持有讀鎖的情況下釋放鎖是不合法的,需要抛出<code>IllegalMonitorStateException</code>異常

<code>Sync</code>類定義了一個<code>HoldCounter</code>變量<code>cachedHoldCounter</code>,用于儲存最近擷取到讀鎖的線程的重入次數。源碼如下:

設計該變量的目的是:将其作為一個緩存,加快代碼執行速度。因為擷取、釋放讀鎖的線程往往都是最近擷取讀鎖的那個線程,雖然每個線程的重入次數都會使用<code>readHolds</code>來儲存,但使用<code>readHolds</code>變量會涉及到<code>ThreadLocal</code>内部的查找(lookup),這是存在一定開銷的。有了<code>cachedHoldCounter</code>這個緩存後,就不用每次都在<code>ThreadLocal</code>内部查找,加快了代碼執行速度。相當于用空間換時間

無論是公平鎖還是非公平鎖,它們擷取鎖的邏輯都是相同的,是以<code>Sync</code>類在這一層就提供了統一的實作

但是,擷取寫鎖和擷取讀鎖的邏輯不相同:

寫鎖是互斥資源,擷取寫鎖的邏輯主要在<code>tryAcquire</code>方法

讀鎖是共享資源,擷取讀鎖的邏輯主要在<code>tryAcquireShared</code>方法

具體的源碼分析見下方的“讀鎖”和“寫鎖”各自章節的“擷取x鎖”部分

無論是公平鎖還是非公平鎖,它們釋放鎖的邏輯都是相同的,是以<code>Sync</code>類在這一層就提供了統一的實作

但是,釋放寫鎖和釋放讀鎖的邏輯不相同:

寫鎖是互斥資源,釋放寫鎖的邏輯主要在<code>tryRelease</code>方法

讀鎖是共享資源,釋放讀鎖的邏輯主要在<code>tryReleaseShared</code>方法

具體的源碼分析見下方的“讀鎖”和“寫鎖”各自章節的“釋放x鎖”部分

寫鎖是由内部類<code>WriteLock</code>實作的,其實作了<code>Lock</code>接口,擷取鎖、釋放鎖的邏輯都委托給了<code>sync</code>域(<code>Sync</code>對象)來執行。<code>WriteLock</code>的基本結構如下:

<code>WriteLock</code>使用<code>lock</code>方法擷取寫鎖,一次擷取一個寫鎖,源碼如下:

<code>lock</code>方法内部實際調用的是AQS的<code>acquire</code>方法,源碼如下:

而<code>acquire</code>方法會調用子類<code>Sync</code>實作的<code>tryAcquire</code>方法,如下:

分為三步:

1、如果讀鎖正在被擷取中,或者寫鎖被擷取中但不是本線程持有,則擷取失敗

2、如果擷取寫鎖達到飽和,則抛出錯誤

3、如果上面兩個都不成立,說明此線程可以請求寫鎖。但需要先根據公平政策來判斷是否應該先阻塞。如果不用阻塞,且CAS成功,則擷取成功。否則擷取失敗

其中公平政策判斷所調用的<code>writerShouldBlock</code>,在後面分析公平鎖和非公平鎖時會給出分析

如果<code>tryAcquire</code>方法擷取寫鎖成功,則<code>acquire</code>方法直接傳回,否則進入同步隊列阻塞等待

<code>tryAcquire</code>展現的讀寫鎖的特征:

互斥關系:

寫鎖和寫鎖之間是互斥的:如果是别的線程持有寫鎖,那麼直接傳回false

讀鎖和寫鎖之間是互斥的。當有線程持有讀鎖時,寫鎖不能獲得:如果<code>c!=0</code>且<code>w==0</code>,說明此時有線程持有讀鎖,直接傳回false

可重入性:如果目前線程持有寫鎖,就不用進行公平性判斷<code>writerShouldBlock</code>,請求鎖一定會擷取成功

不允許鎖更新:如果目前線程持有讀鎖,想要直接申請寫鎖,此時<code>c!=0</code>且<code>w==0</code>,而<code>exclusiveOwnerThread</code>是null,不等于<code>current</code>,直接傳回false

<code>WriteLock</code>使用<code>unlock</code>方法釋放寫鎖,如下:

<code>unlock</code>内部實際上調用的是AQS的<code>release</code>方法,源碼如下:

而該方法會調用子類<code>Sync</code>實作的<code>tryAcquire</code>方法,源碼如下:

注意: 任何鎖的釋放都需要判斷是否是在持有鎖的情況下。如果不持有鎖就釋放,會抛出異常。對于寫鎖來說,判斷是否持有鎖很簡單,隻需要調用<code>isHeldExclusively</code>方法進行判斷即可;而對于讀鎖來說,判斷是否持有鎖比較複雜,需要根據每個線程各自儲存的持有讀鎖數來判斷,即<code>readHolds</code>中儲存的變量

<code>WriteLock</code>使用<code>tryLock</code>來嘗試擷取寫鎖,如下:

<code>tryLock</code>内部實際調用的是<code>Sync</code>類定義并實作的<code>tryWriteLock</code>方法。該方法是一個<code>final</code>方法,不允許子類重寫。其源碼如下:

其實除了缺少對公平政策判斷<code>writerShouldBlock</code>的調用以外,和<code>tryAcquire</code>方法基本上是一樣的,這裡不再廢話

寫鎖支援建立條件變量,因為寫鎖是獨占鎖,而條件變量在<code>await</code>時會釋放掉所有鎖資源。寫鎖能夠保證所有的鎖資源都是本線程所持有,是以可以放心地去釋放所有的鎖

而讀鎖不支援建立條件變量,因為讀鎖是共享鎖,可能會有其他線程持有讀鎖。如果調用<code>await</code>,不僅會釋放掉本線程持有的讀鎖,也會釋放掉其他線程持有的讀鎖,這是不被允許的。是以讀鎖不支援條件變量

讀鎖是由内部類<code>ReadLock</code>實作的,其實作了<code>Lock</code>接口,擷取鎖、釋放鎖的邏輯都委托給了<code>Sync</code>類執行個體<code>sync</code>來執行。<code>ReadLock</code>的基本結構如下:

<code>ReadLock</code>使用<code>lock</code>方法擷取讀鎖,一次擷取一個讀鎖。源碼如下:

<code>lock</code>方法内部實際調用的是AQS的<code>acquireShared</code>方法,源碼如下:

該方法會調用<code>Sync</code>類實作的<code>tryAcquireShared</code>方法,源碼如下:

<code>tryAcquireShared</code>的傳回值說明: 負數:擷取失敗,線程會進入同步隊列阻塞等待 0:擷取成功,但是後續以共享模式擷取的線程都不可能擷取成功(這裡暫時用不上) 正數:擷取成功,且後續以共享模式擷取的線程也可能擷取成功

在讀寫鎖中,<code>tryAcquireShared</code>沒有傳回0的情況,隻會傳回正數或負數

前面“<code>Sync</code>類”中講解過這些變量,這裡再複習一遍:

<code>firstReader</code>、<code>firstReaderHoldCount</code>分别用于記錄第一個擷取到寫鎖的線程及其持有讀鎖的數量

<code>cachedHoldCounter</code>用于記錄最近擷取到寫鎖的線程持有讀鎖的數量

<code>readHolds</code>是一個線程局部變量(<code>ThreadLocal</code>變量),用于儲存每個獲得讀鎖的線程各自持有的讀鎖數量

<code>tryAcquireShared</code>的流程如下:

1、如果其他線程持有寫鎖,那麼擷取失敗(傳回-1)

2、否則,根據公平政策判斷是否應該阻塞。如果不用阻塞且讀鎖數量未飽和,則CAS請求讀鎖。如果CAS成功,擷取成功(傳回1),并記錄相關資訊

3、如果根據公平政策判斷應該阻塞,或者讀鎖數量飽和,或者CAS競争失敗,那麼交給完整版本的擷取方法<code>fullTryAcquireShared</code>去處理

其中上述步驟2如果發生了重入讀(目前線程持有讀鎖的情況下,再次請求讀鎖),但根據公平政策判斷該線程需要阻塞等待,而導緻重入讀失敗。按照正常邏輯,重入讀不應該失敗。不過,<code>tryAcquireShared</code>并沒有處理這種情況,而是将其放到了<code>fullTryAcquireShared</code>中進行處理。此外,CAS競争失敗而導緻擷取讀鎖失敗,也交給<code>fullTryAcquireShared</code>去處理(<code>fullTryAcquireShared</code>表示我好難-_-)

<code>fullTryAcquireShared</code>方法是嘗試擷取讀鎖的完全版本,用于處理<code>tryAcquireShared</code>方法未處理的:

1、CAS競争失敗

2、因公平政策判斷應該阻塞而導緻的重入讀失敗

這兩種情況。其源碼如下:

<code>fullTryAcquireShared</code>其實和<code>tryAcquire</code>存在很多的備援之處,但這麼做的目的主要是讓<code>tryAcquireShared</code>變得更簡單,不用處理複雜的CAS循環

<code>fullTryAcquireShared</code>主要是為了處理CAS失敗和<code>readerShouldBlock</code>判true而導緻的重入讀失敗,這兩種情況在理論上都應該成功擷取鎖。<code>fullTryAcquireShared</code>的做法就是将這兩種情況放在<code>for</code>循環中,一旦發生就重新循環,直到成功為止

<code>tryAcquireShared</code>和<code>fullTryAcquireShared</code>展現的讀寫鎖特征:

讀鎖和讀鎖之間是共享的:即使有其他線程持有了讀鎖,目前線程也能擷取讀鎖

讀鎖和寫鎖之間是互斥的。當有其他線程持有寫鎖,讀鎖不能獲得:<code>tryAcquireShared</code>第4-6行,<code>fullTryAcquireShared</code>第5-7行都能展現這一特征

可重入性:如果目前線程擷取了讀鎖,那麼它再次申請讀鎖一定能成功。這部分邏輯是由<code>fullTryAcquireShared</code>的<code>for</code>循環實作的

支援鎖降級:如果目前線程持有寫鎖,那麼它申請讀鎖一定會成功。這部分邏輯見<code>tryAcquireShared</code>第5行,<code>current</code>和<code>exclusiveOwnerThread</code>是相等的,不會傳回-1

<code>ReadLock</code>使用<code>unlock</code>方法釋放讀鎖,如下:

<code>unlock</code>方法實際調用的是AQS的<code>releaseShared</code>方法,如下:

而該方法會調用<code>Sync</code>類實作的<code>tryReleaseShared</code>方法,源碼如下:

如果傳回true,說明鎖是空閑的,<code>releaseShared</code>方法會進一步調用<code>doReleaseShared</code>方法,<code>doReleaseShared</code>方法會喚醒後繼線程并確定傳播(確定傳播:保證被喚醒的線程可以執行喚醒其後續線程的邏輯)

<code>ReadLock</code>使用<code>tryLock</code>方法嘗試釋放讀鎖,源碼如下:

<code>tryLock</code>内部實際調用的是<code>Sync</code>類定義并實作的<code>tryReadLock</code>方法。該方法是一個<code>final</code>方法,不允許子類重寫。其源碼如下:

其實除了缺少對公平政策判斷方法<code>readerShouldBlock</code>的調用以外,和<code>tryAcquireShared</code>方法基本上是一樣的

和寫鎖的差別在于,讀鎖不支援建立條件變量。如果調用<code>newCondition</code>方法,會直接抛出<code>UnsupportedOperationException</code>異常。不支援的原因在前面已經分析過,這裡不再贅述

<code>ReentrantReadWriteLock</code>預設構造方法如下:

說明其預設建立的是非公平讀寫鎖。如果要建立公平讀寫鎖,需要使用有參構造函數,參數fair設定為true

公平讀寫鎖依賴于<code>Sync</code>的子類<code>FairSync</code>來實作,其源碼如下:

<code>writerShouldBlock</code>實際上調用的是AQS的<code>hasQueuedPredecessors</code>方法,該方法會檢查是否有線程在同步隊列中等待,源碼如下:

<code>writerShouldBlock</code>隻有在<code>tryAcquire</code>中被調用。如果目前線程請求寫鎖時發現已經有線程(讀線程or寫線程)在同步隊列中等待,則讓步

<code>readerShouldBlock</code>和<code>writerShouldBlock</code>一樣,都是調用AQS的<code>hasQueuedPredecessors</code>方法

<code>readerShouldBlock</code>隻有在<code>tryAcquireShared</code>(<code>fullTryAcquireShared</code>)中被調用。如果目前線程請求讀鎖時發現已經有線程(讀線程or寫線程)在同步隊列中等待,則讓步

非公平讀寫鎖依賴于<code>Sync</code>的子類<code>NonfairSync</code>來實作,其源碼如下:

<code>writerShouldBlock</code>直接傳回false

<code>writerShouldBlock</code>隻有在<code>tryAcquire</code>中被調用,傳回false表示在非公平模式下,不管是否有線程在同步隊列中等待,請求寫鎖都不會讓步,而是直接上去競争

<code>readerShouldBlock</code>實際調用的是AQS的<code>apparentlyFirstQueuedIsExclusive</code>方法。其源碼如下:

如果同步隊列為空,或隊首線程是讀線程(擷取讀鎖而被阻塞),則傳回false。如果同步隊列隊首線程是寫線程(擷取寫鎖而被阻塞),則傳回true

<code>readerShouldBlock</code>隻有在<code>tryAcquireShared</code>(<code>fullTryAcquireShared</code>)中被調用。如果目前線程請求讀鎖時發現同步隊列隊首線程是寫線程,則讓步。如果是讀線程則跟它争奪鎖資源

這麼做的目的是為了防止寫線程被“餓死”。因為如果一直有讀線程前來請求鎖,且讀鎖是有求必應,就會使得在同步隊列中的寫線程一直不能被喚醒。不過,<code>apparentlyFirstQueuedIsExclusive</code>隻是一種啟發式算法,并不能保證寫線程一定不會被餓死。因為寫線程有可能不在同步隊列隊首,而是排在其他讀線程後面

公平模式:

無論目前線程請求寫鎖還是讀鎖,隻要發現此時還有别的線程在同步隊列中等待(寫鎖or讀鎖),都一律選擇讓步

非公平模式:

請求寫鎖時,目前線程會選擇直接競争,不會做絲毫的讓步

請求讀鎖時,如果發現同步隊列隊首線程在等待擷取寫鎖,則會讓步。不過這是一種啟發式算法,因為寫線程可能排在其他讀線程後面

如果覺得作者寫的還可以的話,可以👍鼓勵一下

願歸來仍是少年!

    作者:酒冽

    出處:https://www.cnblogs.com/frankiedyz/p/15655865.html

    版權:本文版權歸作者和部落格園共有

    轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接配接;否則必究法律責任