摘要:鎖實現分析本節通過學習源碼分析可重入讀寫鎖的實現。讀寫鎖結構分析繼承于,其中主要功能均在中完成,其中最重要功能為控制線程獲取鎖失敗后轉換為等待狀態及在滿足一定條件后喚醒等待狀態的線程。
概述
本文主要分析JCU包中讀寫鎖接口(ReadWriteLock)的重要實現類ReentrantReadWriteLock。主要實現讀共享,寫互斥功能,對比單純的互斥鎖在共享資源使用場景為頻繁讀取及少量修改的情況下可以較好的提高性能。
ReadWriteLock接口簡單說明ReadWriteLock接口只定義了兩個方法:
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接口一樣使用(其實也有一些特性是不支持的)。
ReentrantReadWriteLock使用示例讀寫鎖的使用并不復雜,可以參考以下使用示例:
class RWDictionary { private final Mapm = new TreeMap (); 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(); } } }
與普通重入鎖使用的主要區別在于需要使用不同的鎖對象引用讀寫鎖,并且在讀寫時分別調用對應的鎖。
ReentrantReadWriteLock鎖實現分析本節通過學習源碼分析可重入讀寫鎖的實現。
圖解重要函數及對象關系根據示例代碼可以發現,讀寫鎖需要關注的重點函數為獲取讀鎖及寫鎖的函數,對于讀鎖及寫鎖對象則主要關注加鎖和解鎖函數,這幾個函數及對象關系如下圖:
從圖中可見讀寫鎖的加鎖解鎖操作最終都是調用ReentrantReadWriteLock類的內部類Sync提供的方法。與{% post_link 細談重入鎖ReentrantLock %}一文中描述相似,Sync對象通過繼承AbstractQueuedSynchronizer進行實現,故后續分析主要基于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,并且主要使用上文介紹的數據結構中的state及waitStatus變量進行實現。
實現讀寫鎖與實現普通互斥鎖的主要區別在于需要分別記錄讀鎖狀態及寫鎖狀態,并且等待隊列中需要區別處理兩種加鎖操作。
Sync使用state變量同時記錄讀鎖與寫鎖狀態,將int類型的state變量分為高16位與第16位,高16位記錄讀鎖狀態,低16位記錄寫鎖狀態,如下圖所示:
Sync使用不同的mode描述等待隊列中的節點以區分讀鎖等待節點和寫鎖等待節點。mode取值包括SHARED及EXCLUSIVE兩種,分別代表當前等待節點為讀鎖和寫鎖。
通過對于重要函數關系的分析,寫鎖加鎖最終調用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已經是有鎖狀態,則需要進一步處理。有鎖狀態主要分為已有寫鎖和已有讀鎖狀態,并且根據最終當前線程是否可直接獲取鎖分為兩種情況:
非重入:如果滿足一下兩個條件之一,當前線程必須加入等待隊列(暫不考慮非公平鎖搶占情況)
a. 已有讀鎖;
b. 有寫鎖且獲取寫鎖的線程不為當前請求鎖的線程。
重入:有寫鎖且當前獲取寫鎖的線程與當前請求鎖的線程為同一線程,則直接獲取鎖并將寫鎖狀態值加1。
寫鎖重入狀態如圖:
寫鎖非重入等待狀態如圖:
在非重入狀態,當前線程創建等待節點追加到等待隊列隊尾,如果當前頭結點為空,則需要創建一個默認的頭結點。
之后再當前獲取鎖的線程釋放鎖后,會喚醒等待中的節點,即為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則喚醒后續第一個等待節點,如喚醒的后續節點為讀等待節點,則后續節點會繼續傳播喚醒狀態。
讀鎖釋放過程讀鎖釋放過比寫鎖稍微復雜,因為是共享鎖,所以可能會有多個線程同時獲取讀鎖,故在解鎖時需要做兩件事:
獲取當前線程對應的重入計數,并進行減1,此處天生為線程安全的,不需要特殊處理;
當前讀鎖獲取次數減1,此處由于可能存在多線程競爭,故使用自旋CAS進行設置。
完成以上兩步后,如讀狀態為0,則喚醒后續等待節點。
總結根據以上分析,本文主要展示了讀寫鎖的場景及方式,并分析讀寫鎖核心功能(加解鎖)的代碼實現。Java讀寫鎖同時附帶了更多其他方法,包括鎖狀態監控和帶超時機制的加鎖方法等,本文不在贅述。并且讀寫鎖中寫鎖可使用Conditon機制也不在詳細說明。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76508.html
摘要:關于,最后有兩點規律需要注意當的等待隊列隊首結點是共享結點,說明當前寫鎖被占用,當寫鎖釋放時,會以傳播的方式喚醒頭結點之后緊鄰的各個共享結點。當的等待隊列隊首結點是獨占結點,說明當前讀鎖被使用,當讀鎖釋放歸零后,會喚醒隊首的獨占結點。 showImg(https://segmentfault.com/img/remote/1460000016012293); 本文首發于一世流云的專欄:...
摘要:所以就有了讀寫鎖。只要沒有,讀取鎖可以由多個線程同時保持。其讀寫鎖為兩個內部類都實現了接口。讀寫鎖同樣依賴自定義同步器來實現同步狀態的,而讀寫狀態就是其自定義同步器的狀態。判斷申請寫鎖數量是否超標超標則直接異常,反之則設置共享狀態。 一、寫在前面 在上篇我們聊到了可重入鎖(排它鎖)ReentrantLcok ,具體參見《J.U.C|可重入鎖ReentrantLock》 Reentra...
摘要:所以就有了讀寫鎖。只要沒有,讀取鎖可以由多個線程同時保持。其讀寫鎖為兩個內部類都實現了接口。讀寫鎖同樣依賴自定義同步器來實現同步狀態的,而讀寫狀態就是其自定義同步器的狀態。判斷申請寫鎖數量是否超標超標則直接異常,反之則設置共享狀態。 一、寫在前面 在上篇我們聊到了可重入鎖(排它鎖)ReentrantLcok ,具體參見《J.U.C|可重入鎖ReentrantLock》 Reentra...
摘要:類顧名思義是一種讀寫鎖它是接口的直接實現該類在內部實現了具體獨占鎖特點的寫鎖以及具有共享鎖特點的讀鎖和一樣類也是通過定義內部類實現框架的來實現獨占共享的功能屬于排他鎖這些鎖在同一時刻只允許一個線程進行訪問但是在大多數場景下大部分時間都是提供 ReentrantReadWriteLock 類, 顧名思義, 是一種讀寫鎖, 它是 ReadWriteLock 接口的直接實現, 該類在內部實現...
摘要:我們知道,的作用其實是對類的和的增強,是為了讓線程在指定對象上等待,是一種線程之間進行協調的工具。當線程調用對象的方法時,必須拿到和這個對象關聯的鎖。 showImg(https://segmentfault.com/img/remote/1460000016012566); 本文首發于一世流云的專欄:https://segmentfault.com/blog... 一、Reentr...
閱讀 3565·2023-04-25 14:20
閱讀 1179·2021-09-10 10:51
閱讀 1146·2019-08-30 15:53
閱讀 452·2019-08-30 15:43
閱讀 2307·2019-08-30 14:13
閱讀 2785·2019-08-30 12:45
閱讀 1199·2019-08-29 16:18
閱讀 1155·2019-08-29 16:12