更新時間:2022-09-23 10:09:08 來源:動力節點 瀏覽2077次
讀/寫鎖是比JavaLock中的鎖文本中顯示的實現更復雜的鎖。想象一下,你有一個應用程序讀取和寫入一些資源,但寫入它的工作不如讀取它。讀取同一資源的兩個線程不會互相造成問題,因此多個想要讀取資源的線程同時被授予訪問權限,重疊。但是,如果單個線程想要寫入資源,則不能同時進行其他讀取或寫入。為了解決這個允許多個讀者但只有一個寫者的問題,你需要一個讀/寫鎖。
首先我們總結一下獲取資源讀寫權限的條件:
讀取權限,如果沒有線程在寫,并且沒有線程請求寫訪問。
寫訪問,如果沒有線程正在讀取或寫入。
如果一個線程想要讀取資源,只要沒有線程正在寫入,并且沒有線程請求對該資源的寫訪問,就可以了。通過提高寫訪問請求的優先級,我們假設寫請求比讀請求更重要。此外,如果讀取是最常發生的事情,并且我們沒有提高寫入的優先級,則可能會發生饑餓。請求寫訪問的線程將被阻止,直到所有讀者都解鎖了ReadWriteLock. 如果新線程不斷被授予讀訪問權限,則等待寫訪問權限的線程將無限期地保持阻塞,從而導致饑餓。因此,如果當前沒有線程鎖定線程,則只能授予線程讀取訪問權限ReadWriteLock寫作,或要求鎖定寫作。
可以授予想要對資源進行寫訪問的線程,因此當沒有線程正在讀取或寫入資源時。有多少線程請求寫訪問或按什么順序都沒有關系,除非您想保證請求寫訪問的線程之間的公平性。
考慮到這些簡單的規則,我們可以實現ReadWriteLock如下所示:
公共類讀寫鎖{
私人 int 讀者 = 0;
私人 int 作家 = 0;
私人 int writeRequests = 0;
公共同步 void lockRead() 拋出 InterruptedException{
而(作家> 0 || writeRequests > 0){
等待();
}
讀者++;
}
公共同步無效解鎖讀取(){
讀者——;
通知所有();
}
公共同步 void lockWrite() 拋出 InterruptedException{
寫請求++;
而(讀者> 0 ||作家> 0){
等待();
}
寫請求——;
作家++;
}
公共同步 void unlockWrite() 拋出 InterruptedException{
作家——;
通知所有();
}
}
有ReadWriteLock兩種鎖定方法和兩種解鎖方法。一種用于讀取訪問的鎖定和解鎖方法,一種用于寫入訪問的鎖定和解鎖方法。
讀取訪問的規則在該lockRead()方法中實現。所有線程都獲得讀訪問權限,除非有一個線程具有寫訪問權限,或者一個或多個線程請求了寫訪問權限。
寫訪問的規則在lockWrite()方法中實現。想要寫訪問的線程從請求寫訪問開始(writeRequests++)。然后它將檢查它是否真的可以獲得寫訪問權限。如果沒有對資源具有讀訪問權限的線程,并且沒有對資源具有寫訪問權限的線程,則線程可以獲得寫訪問權限。有多少線程請求寫訪問并不重要。
值得注意的是,兩者都是unlockRead()andunlockWrite()調用 notifyAll()而不是notify(). 要解釋為什么會這樣,請想象以下情況:
在 ReadWriteLock 內部有等待讀訪問的線程和等待寫訪問的線程。如果被喚醒的線程notify()是讀訪問線程,它將被放回等待,因為有線程在等待寫訪問。但是,沒有一個等待寫訪問的線程被喚醒,所以沒有更多的事情發生。沒有線程既不能讀也不能寫。通過調用noftifyAll()喚醒所有等待的線程并檢查它們是否可以獲得所需的訪問權限。
打電話notifyAll()還有另一個好處。如果多個線程正在等待讀取訪問,而沒有一個線程正在等待寫入訪問,并且unlockWrite()被調用,則所有等待讀取訪問的線程都被立即授予讀取訪問權限 - 而不是一個接一個。
前面顯示的ReadWriteLock類是不可重入的。如果一個具有寫訪問權限的線程再次請求它,它將阻塞,因為已經有一個寫者——它自己。此外,考慮這種情況:
線程 1 獲得讀取權限。
線程 2 請求寫訪問,但由于只有一個讀取器而被阻止。
線程1重新請求讀訪問(重新入鎖),但是因為有寫請求而被阻塞
在這種情況下,前一個ReadWriteLock會鎖定 - 類似于死鎖的情況。不會授予既不請求讀取也不請求寫入訪問的線程。
要使ReadWriteLock可重入,有必要進行一些更改。讀者和作者的重入將分別處理。
讀取重入
為了讓ReadWriteLock讀者可以重入,我們首先要建立閱讀重入的規則:
如果線程可以獲得讀取訪問權限(沒有寫入者或寫入請求),或者如果它已經具有讀取訪問權限(無論寫入請求如何),它就會被授予讀取重入權限。
為了確定一個線程是否已經具有讀訪問權限,對每個被授予讀訪問權限的線程的引用以及它獲得讀鎖的次數都保存在 Map 中。在確定是否可以授予讀取訪問權限時,將檢查此 Map 是否對調用線程的引用。以下是更改后lockRead()andunlockRead()方法的外觀:
公共類讀寫鎖{
私有 Map<Thread, Integer> readingThreads =
新的 HashMap<Thread, Integer>();
私人 int 作家 = 0;
私人 int writeRequests = 0;
公共同步 void lockRead() 拋出 InterruptedException{
線程調用Thread = Thread.currentThread();
而(!canGrantReadAccess(調用線程)){
等待();
}
readingThreads.put(調用線程,
(getAccessCount(callingThread) + 1));
}
公共同步無效解鎖讀取(){
線程調用Thread = Thread.currentThread();
int accessCount = getAccessCount(callingThread);
if(accessCount == 1){ readingThreads.remove(callingThread); }
否則 { readingThreads.put(callingThread, (accessCount -1)); }
通知所有();
}
私有布爾canGrantReadAccess(線程調用線程){
如果(作家> 0)返回假;
如果(isReader(調用線程)返回真;
如果(writeRequests > 0)返回假;
返回真;
}
私有 int getReadAccessCount(線程調用線程){
整數 accessCount = readingThreads.get(callingThread);
if(accessCount == null) 返回 0;
返回 accessCount.intValue();
}
私有布爾 isReader(線程調用線程){
返回閱讀Threads.get(callingThread) != null;
}
}
如您所見,僅當當前沒有線程寫入資源時才授予讀取重入。此外,如果調用線程已經具有讀取訪問權限,則這優先于任何 writeRequests。
僅當線程已經具有寫訪問權限時才授予寫重入。以下是更改后的lockWrite()andunlockWrite()方法:
公共類讀寫鎖{
私有 Map<Thread, Integer> readingThreads =
新的 HashMap<Thread, Integer>();
私有 int writeAccesses = 0;
私人 int writeRequests = 0;
私有線程寫作Thread = null;
公共同步 void lockWrite() 拋出 InterruptedException{
寫請求++;
線程調用Thread = Thread.currentThread();
而(!canGrantWriteAccess(調用線程)){
等待();
}
寫請求——;
寫訪問++;
寫線程 = 調用線程;
}
公共同步 void unlockWrite() 拋出 InterruptedException{
寫訪問——;
如果(writeAccesses == 0){
寫線程=空;
}
通知所有();
}
私有布爾canGrantWriteAccess(線程調用線程){
如果(hasReaders())返回假;
if(writingThread == null) 返回真;
if(!isWriter(callingThread)) 返回假;
返回真;
}
私有布爾 hasReaders(){
返回讀數Threads.size() > 0;
}
私有布爾 isWriter(線程調用線程){
返回寫線程 == 調用線程;
}
}
請注意,在確定調用線程是否可以獲得寫訪問權時,現在如何考慮當前持有寫鎖的線程。
有時,具有讀訪問權限的線程也需要獲得寫訪問權限。為此,線程必須是唯一的讀者。為了實現這一點,writeLock()應該稍微改變方法。這是它的樣子:
公共類讀寫鎖{
私有 Map<Thread, Integer> readingThreads =
新的 HashMap<Thread, Integer>();
私有 int writeAccesses = 0;
私人 int writeRequests = 0;
私有線程寫作Thread = null;
公共同步 void lockWrite() 拋出 InterruptedException{
寫請求++;
線程調用Thread = Thread.currentThread();
而(!canGrantWriteAccess(調用線程)){
等待();
}
寫請求——;
寫訪問++;
寫線程 = 調用線程;
}
公共同步 void unlockWrite() 拋出 InterruptedException{
寫訪問——;
如果(writeAccesses == 0){
寫線程=空;
}
通知所有();
}
私有布爾canGrantWriteAccess(線程調用線程){
if(isOnlyReader(callingThread)) 返回真;
如果(hasReaders())返回假;
if(writingThread == null) 返回真;
if(!isWriter(callingThread)) 返回假;
返回真;
}
私有布爾 hasReaders(){
返回讀數Threads.size() > 0;
}
私有布爾 isWriter(線程調用線程){
返回寫線程 == 調用線程;
}
私有布爾 isOnlyReader(線程線程){
返回讀數Threads.size() == 1 &&
readingThreads.get(callingThread) != null;
}
}
現在ReadWriteLock該類是讀寫訪問可重入的。
有時,具有寫訪問權限的線程也需要讀訪問權限。如果請求,應始終授予寫入者讀取訪問權限。如果一個線程有寫訪問權限,其他線程就不能有讀或寫訪問權限,所以它并不危險。以下是該 canGrantReadAccess()方法在更改后的外觀:
公共類讀寫鎖{
私有布爾canGrantReadAccess(線程調用線程){
if(isWriter(callingThread)) 返回真;
如果(寫線程!= null)返回false;
如果(isReader(調用線程)返回真;
如果(writeRequests > 0)返回假;
返回真;
}
}
下面是完全可重入的ReadWriteLock實現。我對訪問條件進行了一些重構,以使它們更易于閱讀,從而更容易說服自己它們是正確的。
公共類讀寫鎖{
私有 Map<Thread, Integer> readingThreads =
新的 HashMap<Thread, Integer>();
私有 int writeAccesses = 0;
私人 int writeRequests = 0;
私有線程寫作Thread = null;
公共同步 void lockRead() 拋出 InterruptedException{
線程調用Thread = Thread.currentThread();
而(!canGrantReadAccess(調用線程)){
等待();
}
readingThreads.put(調用線程,
(getReadAccessCount(callingThread) + 1));
}
私有布爾canGrantReadAccess(線程調用線程){
if( isWriter(callingThread) ) 返回真;
if( hasWriter() ) 返回假;
if( isReader(callingThread) ) 返回真;
if( hasWriteRequests() ) 返回假;
返回真;
}
公共同步無效解鎖讀取(){
線程調用Thread = Thread.currentThread();
如果(!isReader(調用線程)){
throw new IllegalMonitorStateException("調用線程沒有" +
" 持有此 ReadWriteLock 的讀鎖");
}
int accessCount = getReadAccessCount(callingThread);
if(accessCount == 1){ readingThreads.remove(callingThread); }
否則 { readingThreads.put(callingThread, (accessCount -1)); }
通知所有();
}
公共同步 void lockWrite() 拋出 InterruptedException{
寫請求++;
線程調用Thread = Thread.currentThread();
而(!canGrantWriteAccess(調用線程)){
等待();
}
寫請求——;
寫訪問++;
寫線程 = 調用線程;
}
公共同步 void unlockWrite() 拋出 InterruptedException{
if(!isWriter(Thread.currentThread()){
throw new IllegalMonitorStateException("調用線程沒有" +
" 持有這個 ReadWriteLock 的寫鎖");
}
寫訪問——;
如果(writeAccesses == 0){
寫線程=空;
}
通知所有();
}
私有布爾canGrantWriteAccess(線程調用線程){
if(isOnlyReader(callingThread)) 返回真;
如果(hasReaders())返回假;
if(writingThread == null) 返回真;
if(!isWriter(callingThread)) 返回假;
返回真;
}
私有 int getReadAccessCount(線程調用線程){
整數 accessCount = readingThreads.get(callingThread);
if(accessCount == null) 返回 0;
返回 accessCount.intValue();
}
私有布爾 hasReaders(){
返回讀數Threads.size() > 0;
}
私有布爾 isReader(線程調用線程){
返回閱讀Threads.get(callingThread) != null;
}
私有布爾 isOnlyReader(線程調用線程){
返回讀數Threads.size() == 1 &&
readingThreads.get(callingThread) != null;
}
私有布爾 hasWriter(){
返回寫作線程!= null;
}
私有布爾 isWriter(線程調用線程){
返回寫線程 == 調用線程;
}
私有布爾 hasWriteRequests(){
返回 this.writeRequests > 0;
}
}
當用 保護臨界區時ReadWriteLock,臨界區可能會拋出異常,從 - 子句內部調用readUnlock()和writeUnlock()方法很重要finally。這樣做可以確保ReadWriteLock已解鎖,以便其他線程可以鎖定它。這是一個例子:
lock.lockWrite();
嘗試{
//做臨界區代碼,可能會拋出異常
} 最后 {
lock.unlockWrite();
}
這個小結構確保ReadWriteLock在關鍵部分的代碼拋出異常的情況下解鎖。如果unlockWrite() 沒有從 - 子句內部調用finally,并且從臨界區拋出異常,ReadWriteLock則將永遠保持寫鎖定,導致調用該實例的所有線程lockRead()或lockWrite()在該 ReadWriteLock實例上無限期停止。唯一可以解鎖的ReadWriteLock方法是如果 ReadWriteLock是可重入的,并且在拋出異常時鎖定它的線程后來成功鎖定它,執行關鍵部分并unlockWrite() 隨后再次調用。那將ReadWriteLock再次解鎖。但為什么要等到這種情況發生,如果它發生了嗎?unlockWrite()從 -子句調用finally是一個更健壯的解決方案。
以上就是關于“Java實現讀寫鎖的原理”介紹,大家如果想了解更多相關知識,不妨來關注一下本站的Java在線學習,里面的課程內容從入門到精通,細致全面,很適合沒有基礎的小伙伴學習,希望對大家能夠有所幫助。
0基礎 0學費 15天面授
有基礎 直達就業
業余時間 高薪轉行
工作1~3年,加薪神器
工作3~5年,晉升架構
提交申請后,顧問老師會電話與您溝通安排學習