本文主要分析JCU包中讀寫鎖接口(ReadWriteLock
)的重要實現類ReentrantReadWriteLock
。主要實現讀共享,寫互斥功能,對比單純的互斥鎖在共享資源使用場景爲頻繁讀取及少許修改的狀況下能夠較好的提升性能。html
ReadWriteLock接口只定義了兩個方法:java
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
經過調用相應方法獲取讀鎖或寫鎖,獲取的讀鎖及寫鎖都是Lock
接口的實現,能夠如同使用Lock
接口同樣使用(其實也有一些特性是不支持的)。node
讀寫鎖的使用並不複雜,能夠參考如下使用示例:web
class RWDictionary { private final Map<String, Data> m = new TreeMap<String, Data>(); private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); private final Lock w = rwl.writeLock(); public Data get(String key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } public String[] allKeys() { r.lock(); try { return m.keySet().toArray(); } finally { r.unlock(); } } public Data put(String key, Data value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } } public void clear() { w.lock(); try { m.clear(); } finally { w.unlock(); } } }
與普通重入鎖使用的主要區別在於須要使用不一樣的鎖對象引用讀寫鎖,而且在讀寫時分別調用對應的鎖。api
本節經過學習源碼分析可重入讀寫鎖的實現。緩存
根據示例代碼能夠發現,讀寫鎖須要關注的重點函數爲獲取讀鎖及寫鎖的函數,對於讀鎖及寫鎖對象則主要關注加鎖和解鎖函數,這幾個函數及對象關係以下圖:
從圖中可見讀寫鎖的加鎖解鎖操做最終都是調用ReentrantReadWriteLock
類的內部類Sync
提供的方法。與{% post_link 細談重入鎖ReentrantLock %}一文中描述類似,Sync
對象經過繼承AbstractQueuedSynchronizer
進行實現,故後續分析主要基於Sync
類進行。安全
Sync
結構分析Sync
繼承於AbstractQueuedSynchronizer
,其中主要功能均在AbstractQueuedSynchronizer
中完成,其中最重要功能爲控制線程獲取鎖失敗後轉換爲等待狀態及在知足必定條件後喚醒等待狀態的線程。先對AbstractQueuedSynchronizer
進行觀察。數據結構
AbstractQueuedSynchronizer
圖解爲了更好理解AbstractQueuedSynchronizer
的運行機制,能夠首先研究其內部數據結構,以下圖:
圖中展現AQS類較爲重要的數據結構,包括int
類型變量state
用於記錄鎖的狀態,繼承自AbstractOwnableSynchronizer
類的Thread
類型變量exclusiveOwnerThread
用於指向當前排他的獲取鎖的線程,AbstractQueuedSynchronizer.Node
類型的變量head
及tail
。
其中Node
對象表示當前等待鎖的節點,Node
中thread
變量指向等待的線程,waitStatus
表示當前等待節點狀態,mode
爲節點類型。多個節點之間使用prev
及next
組成雙向鏈表,參考CLH鎖隊列的方式進行鎖的獲取,但其中與CLH隊列的重要區別在於CLH隊列中後續節點須要自旋輪詢前節點狀態以肯定前置節點是否已經釋放鎖,期間不釋放CPU資源,而AQS
中Node
節點指向的線程在獲取鎖失敗後調用LockSupport.park
函數使其進入阻塞狀態,讓出CPU資源,故在前置節點釋放鎖時須要調用unparkSuccessor
函數喚醒後繼節點。
根據以上說明可得知此上圖圖主要表現當前thread0
線程獲取了鎖,thread1
線程正在等待。多線程
Sync
對於AQS
使用讀寫鎖中Sync
類是繼承於AQS
,而且主要使用上文介紹的數據結構中的state
及waitStatus
變量進行實現。
實現讀寫鎖與實現普通互斥鎖的主要區別在於須要分別記錄讀鎖狀態及寫鎖狀態,而且等待隊列中須要區別處理兩種加鎖操做。 Sync
使用state
變量同時記錄讀鎖與寫鎖狀態,將int
類型的state
變量分爲高16位與第16位,高16位記錄讀鎖狀態,低16位記錄寫鎖狀態,以下圖所示:Sync
使用不一樣的mode
描述等待隊列中的節點以區分讀鎖等待節點和寫鎖等待節點。mode
取值包括SHARED
及EXCLUSIVE
兩種,分別表明當前等待節點爲讀鎖和寫鎖。oracle
Sync
代碼過程分析經過對於重要函數關係的分析,寫鎖加鎖最終調用Sync
類的acquire
函數(繼承自AQS
)
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
如今分狀況圖解分析
無鎖狀態AQS
內部數據結構以下圖所示:
其中state
變量爲0,表示高位地位地位均爲0,沒有任何鎖,且等待節點的首尾均指向空(此處特指head節點沒有初始化時),鎖的全部者線程也爲空。
在無鎖狀態進行加鎖操做,線程調用acquire
函數,首先使用tryAcquire
函數判斷鎖是否可獲取成功,因爲當前是無鎖狀態必然成功獲取鎖(若是多個線程同時進入此函數,則有且只有一個線程可調用compareAndSetState
成功,其餘線程轉入獲取鎖失敗的流程)。獲取鎖成功後AQS
狀態爲:
在加寫鎖時若是當前AQS
已是有鎖狀態,則須要進一步處理。有鎖狀態主要分爲已有寫鎖和已有讀鎖狀態,而且根據最終當前線程是否可直接獲取鎖分爲兩種狀況:
寫鎖重入狀態如圖:
寫鎖非重入等待狀態如圖:
在非重入狀態,當前線程建立等待節點追加到等待隊列隊尾,若是當前頭結點爲空,則須要建立一個默認的頭結點。
以後再當前獲取鎖的線程釋放鎖後,會喚醒等待中的節點,即爲thread1
。若是當前等待隊列存在多個等待節點,因爲thread1
等待節點爲EXCLUSIVE
模式,則只會喚醒當前一個節點,不會傳播喚醒信號。
經過對於重要函數關係的分析,寫鎖加鎖最終調用Sync
類的acquireShared
函數(繼承自AQS
):
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
同上文,如今分狀況圖解分析
無所狀態AQS
內部數據狀態圖與寫加鎖是無鎖狀態一致:
在無鎖狀態進行加鎖操做,線程調用acquireShared
函數,首先使用tryAcquireShared
函數判斷共享鎖是否可獲取成功,因爲當前爲無鎖狀態則獲取鎖必定成功(若是同時多個線程在讀鎖進行競爭,則只有一個線程可以直接獲取讀鎖,其餘線程須要進入fullTryAcquireShared
函數繼續進行鎖的獲取,該函數在後文說明)。當前線程獲取讀鎖成功後,AQS
內部結構如圖所示:
其中有兩個新的變量:firstReader
及firstReaderHoldCount
。firstReader
指向在無鎖狀態下第一個獲取讀鎖的線程,firstReaderHoldCount
記錄第一個獲取讀鎖的線程持有當前鎖的計數(主要用於重入)。
無鎖狀態獲取讀鎖比較簡單,在有鎖狀態則須要分狀況討論。其中須要分當前被持有的鎖是讀鎖仍是寫鎖,而且每種狀況須要區分等待隊列中是否有等待節點。
此狀態比較簡單,圖示如:此時線程申請讀鎖,首先調用
readerShouldBlock
函數進行判斷,該函數根據當前鎖是否爲公平鎖判斷規則稍有不一樣。若是爲非公平鎖,則只須要當前第一個等待節點不是寫鎖就能夠嘗試獲取鎖(考慮第一點爲寫鎖主要爲了方式寫鎖「餓死」);若是是公平鎖則只要有等待節點且當前鎖不爲重入就須要等待。
因爲本節的前提是等待隊列爲空的狀況,故readerShouldBlock
函數必定返回false
,則當前線程使用CAS
對讀鎖計數進行增長(同上文,若是同時多個線程在讀鎖進行競爭,則只有一個線程可以直接獲取讀鎖,其餘線程須要進入fullTryAcquireShared
函數繼續進行鎖的獲取)。
在成功對讀鎖計數器進行增長後,當前線程須要繼續對當前線程持有讀鎖的計數進行增長。此時分爲兩種狀況:
firstReader
及firstReaderHoldCount
兩個變量進行存儲,則僅僅須要將firstReaderHoldCount
加1便可;readHolds
進行存儲,readHolds
是ThreadLocal
的子類,經過readHolds
可獲取當前線程對應的HoldCounter
類的對象,該對象保存了當前線程獲取讀鎖的計數。考慮程序的局部性原理,又使用cachedHoldCounter
緩存最近使用的HoldCounter
類的對象,如在一段時間內只有一個線程請求讀鎖則可加速對讀鎖獲取的計數。第一個讀鎖線程重入如圖:
非首節點獲取讀鎖
根據上圖所示,thread0
爲首節點,thread1
線程繼續申請讀鎖,獲取成功後使用ThreadLocal
連接的方式進行存儲計數對象,而且因爲其爲最近獲取讀鎖的線程,則cachedHoldCounter
對象設置指向thread1
對應的計數對象。
在當前鎖已經被讀鎖獲取,且等待隊列不爲空的狀況下 ,可知等待隊列的頭結點必定爲寫鎖獲取等待,這是因爲在讀寫鎖實現過程當中,若是某線程獲取了讀鎖,則會喚醒當前等到節點以後的全部等待模式爲SHARED
的節點,直到隊尾或遇到EXCLUSIVE
模式的等待節點(具體實現函數爲setHeadAndPropagate
後續還會遇到)。因此能夠肯定當前爲讀鎖狀態其有等待節點狀況下,首節點必定是寫鎖等待。如圖所示:
上圖展現當前thread0
與thread1
線程獲取讀鎖,thread0
爲首個獲取讀鎖的節點,而且thread2
線程在等待獲取寫鎖。
在上圖顯示的狀態下,不管公平鎖仍是非公平鎖的實現,新的讀鎖加鎖必定會進行排隊,添加等待節點在寫鎖等待節點以後,這樣能夠防止寫操做的餓死。申請讀鎖後的狀態如圖所示:
如圖所示,在當前鎖被爲讀鎖且有等待隊列狀況下,thread3
及thread4
線程申請讀鎖,則被封裝爲等待節點追加到當前等待隊列後,節點模式爲SHARED
,線程使用LockSupport.park
函數進入阻塞狀態,讓出CPU資源,直到前驅的等待節點完成鎖的獲取和釋放後進行喚醒。
當前線程申請讀鎖時發現寫鎖已經被獲取,則不管等待隊列是否爲空,線程必定會須要加入等待隊列(注意在非公平鎖實現且前序沒有寫鎖申請的等待,線程有機會搶佔獲取鎖而不進入等待隊列)。寫鎖被獲取的狀況下,AQS
狀態爲以下狀態
在兩種狀況下,讀鎖獲取都會進入等待隊列等待前序節點喚醒,這裏再也不贅述。
讀寫鎖與單純的排他鎖主要區別在於讀鎖的共享性,在讀寫鎖實現中保證讀鎖可以共享的其中一個機制就在於,若是一個讀鎖等待節點被喚醒後其會繼續喚醒拍在當前喚醒節點以後的SHARED
模式等待節點。查看源碼:
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) { //注意看這裏 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); } }
在for
循環中,線程若是獲取讀鎖成功後,須要調用setHeadAndPropagate
方法。查看其源碼:
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }
在知足傳播條件狀況下,獲取讀鎖後繼續喚醒後續節點,因此若是當前鎖是讀鎖狀態則等待節點第一個節點必定是寫鎖等待節點。
鎖降級算是獲取讀鎖的特例,如在t0
線程已經獲取寫鎖的狀況下,再調取讀鎖加鎖函數則能夠直接獲取讀鎖,但此時其餘線程仍然沒法獲取讀鎖或寫鎖,在t0
線程釋放寫鎖後,若是有節點等待則會喚醒後續節點,後續節點可見的狀態爲目前有t0
線程獲取了讀鎖。
所降級有什麼應用場景呢?引用讀寫鎖中使用示例代碼
class CachedData { Object data; volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); try { // Recheck state because another thread might have // acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { rwl.readLock().unlock(); } } }
其中針對變量cacheValid
的使用主要過程爲加讀鎖、讀取、釋放讀鎖、加寫鎖、修改值、加讀鎖、釋放寫鎖、使用數據、釋放讀鎖。其中後續幾步(加寫鎖、修改值、加讀鎖、釋放寫鎖、使用數據、釋放讀鎖)爲典型的鎖降級。若是不使用鎖降級,則過程可能有三種狀況:
t0
)獲取寫鎖準備修改(尚未改)cacheValid
變量,而當前線程卻繼續運行,則當前線程讀到的cacheValid
變量的值爲t0
修改前的老數據;所以針對以上狀況提供了鎖的降級功能,能夠在完成數據修改後儘快讀取最新的值,且可以減小寫鎖佔用時間。
最後注意,讀寫鎖不支持鎖升級,即獲取讀鎖、讀數據、獲取寫鎖、釋放讀鎖、釋放寫鎖這個過程,由於讀鎖爲共享鎖,如同時有多個線程獲取了讀鎖後有一個線程進行鎖升級獲取了寫鎖,這會形成同時有讀鎖(其餘線程)和寫鎖的狀況,形成其餘線程可能沒法感知新修改的數據(此爲邏輯性錯誤),而且在JAVA讀寫鎖實現上因爲當前線程獲取了讀鎖,再次請求寫鎖時必然會阻塞而致使後續釋放讀鎖的方法沒法執行,這回形成死鎖(此爲功能性錯誤)。
瞭解了加鎖過程後解鎖過程就很是簡單,每次調用解鎖方法都會減小重入計數次數,直到減爲0則喚醒後續第一個等待節點,如喚醒的後續節點爲讀等待節點,則後續節點會繼續傳播喚醒狀態。
讀鎖釋放過比寫鎖稍微複雜,由於是共享鎖,因此可能會有多個線程同時獲取讀鎖,故在解鎖時須要作兩件事:
CAS
進行設置。完成以上兩步後,如讀狀態爲0,則喚醒後續等待節點。
根據以上分析,本文主要展現了讀寫鎖的場景及方式,並分析讀寫鎖核心功能(加解鎖)的代碼實現。Java讀寫鎖同時附帶了更多其餘方法,包括鎖狀態監控和帶超時機制的加鎖方法等,本文不在贅述。而且讀寫鎖中寫鎖可以使用Conditon
機制也不在詳細說明。