摘要:如果這時其中一個等待讀鎖的線程被方法喚醒,但因?yàn)榇藭r仍有請求寫鎖的線程存在,所以被喚醒的線程會再次進(jìn)入阻塞狀態(tài)。想要允許這樣的操作,要求這個線程是唯一一個擁有讀鎖的線程。
假設(shè)你的程序中涉及到對一些共享資源的讀和寫操作,且寫操作沒有讀操作那么頻繁。在沒有寫操作的時候,兩個線程同時讀一個資源沒有任何問題,所以應(yīng)該允許多個線程能在同時讀取共享資源。但是如果有一個線程想去寫這些共享資源,就不應(yīng)該再有其它線程對該資源進(jìn)行讀或?qū)懀?em>譯者注:也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。這就需要一個讀/寫鎖來解決這個問題。
Java5在java.util.concurrent包中已經(jīng)包含了讀寫鎖。盡管如此,我們還是應(yīng)該了解其實(shí)現(xiàn)背后的原理。
讀/寫鎖的Java實(shí)現(xiàn)先讓我們對讀寫訪問資源的條件做個概述:
讀取 沒有線程正在做寫操作,且沒有線程在請求寫操作。
寫入 沒有線程正在做讀寫操作。
如果某個線程想要讀取資源,只要沒有線程正在對該資源進(jìn)行寫操作且沒有線程請求對該資源的寫操作即可。我們假設(shè)對寫操作的請求比對讀操作的請求更重要,就要提升寫請求的優(yōu)先級。此外,如果讀操作發(fā)生的比較頻繁,我們又沒有提升寫操作的優(yōu)先級,那么就會產(chǎn)生“饑餓”現(xiàn)象。請求寫操作的線程會一直阻塞,直到所有的讀線程都從ReadWriteLock上解鎖了。如果一直保證新線程的讀操作權(quán)限,那么等待寫操作的線程就會一直阻塞下去,結(jié)果就是發(fā)生“饑餓”。因此,只有當(dāng)沒有線程正在鎖住ReadWriteLock進(jìn)行寫操作,且沒有線程請求該鎖準(zhǔn)備執(zhí)行寫操作時,才能保證讀操作繼續(xù)。
當(dāng)其它線程沒有對共享資源進(jìn)行讀操作或者寫操作時,某個線程就有可能獲得該共享資源的寫鎖,進(jìn)而對共享資源進(jìn)行寫操作。有多少線程請求了寫鎖以及以何種順序請求寫鎖并不重要,除非你想保證寫鎖請求的公平性。
按照上面的敘述,簡單的實(shí)現(xiàn)出一個讀/寫鎖,代碼如下
public class ReadWriteLock{ private int readers = 0; private int writers = 0; private int writeRequests = 0; public synchronized void lockRead() throws InterruptedException{ while(writers > 0 || writeRequests > 0){ wait(); } readers++; } public synchronized void unlockRead(){ readers--; notifyAll(); } public synchronized void lockWrite() throws InterruptedException{ writeRequests++; while(readers > 0 || writers > 0){ wait(); } writeRequests--; writers++; } public synchronized void unlockWrite() throws InterruptedException{ writers--; notifyAll(); } }
ReadWriteLock類中,讀鎖和寫鎖各有一個獲取鎖和釋放鎖的方法。
讀鎖的實(shí)現(xiàn)在lockRead()中,只要沒有線程擁有寫鎖(writers==0),且沒有線程在請求寫鎖(writeRequests ==0),所有想獲得讀鎖的線程都能成功獲取。
寫鎖的實(shí)現(xiàn)在lockWrite()中,當(dāng)一個線程想獲得寫鎖的時候,首先會把寫鎖請求數(shù)加1(writeRequests++),然后再去判斷是否能夠真能獲得寫鎖,當(dāng)沒有線程持有讀鎖(readers==0 ),且沒有線程持有寫鎖(writers==0)時就能獲得寫鎖。有多少線程在請求寫鎖并無關(guān)系。
需要注意的是,在兩個釋放鎖的方法(unlockRead,unlockWrite)中,都調(diào)用了notifyAll方法,而不是notify。要解釋這個原因,我們可以想象下面一種情形:
如果有線程在等待獲取讀鎖,同時又有線程在等待獲取寫鎖。如果這時其中一個等待讀鎖的線程被notify方法喚醒,但因?yàn)榇藭r仍有請求寫鎖的線程存在(writeRequests>0),所以被喚醒的線程會再次進(jìn)入阻塞狀態(tài)。然而,等待寫鎖的線程一個也沒被喚醒,就像什么也沒發(fā)生過一樣(譯者注:信號丟失現(xiàn)象)。如果用的是notifyAll方法,所有的線程都會被喚醒,然后判斷能否獲得其請求的鎖。
用notifyAll還有一個好處。如果有多個讀線程在等待讀鎖且沒有線程在等待寫鎖時,調(diào)用unlockWrite()后,所有等待讀鎖的線程都能立馬成功獲取讀鎖 —— 而不是一次只允許一個。
讀/寫鎖的重入上面實(shí)現(xiàn)的讀/寫鎖(ReadWriteLock) 是不可重入的,當(dāng)一個已經(jīng)持有寫鎖的線程再次請求寫鎖時,就會被阻塞。原因是已經(jīng)有一個寫線程了——就是它自己。此外,考慮下面的例子:
Thread 1 獲得了讀鎖
Thread 2 請求寫鎖,但因?yàn)門hread 1 持有了讀鎖,所以寫鎖請求被阻塞。
Thread 1 再想請求一次讀鎖,但因?yàn)門hread
2處于請求寫鎖的狀態(tài),所以想再次獲取讀鎖也會被阻塞。
上面這種情形使用前面的ReadWriteLock就會被鎖定——一種類似于死鎖的情形。不會再有線程能夠成功獲取讀鎖或?qū)戞i了。
為了讓ReadWriteLock可重入,需要對它做一些改進(jìn)。下面會分別處理讀鎖的重入和寫鎖的重入。
讀鎖重入為了讓ReadWriteLock的讀鎖可重入,我們要先為讀鎖重入建立規(guī)則:
要保證某個線程中的讀鎖可重入,要么滿足獲取讀鎖的條件(沒有寫或?qū)懻埱螅匆呀?jīng)持有讀鎖(不管是否有寫請求)。
要確定一個線程是否已經(jīng)持有讀鎖,可以用一個map來存儲已經(jīng)持有讀鎖的線程以及對應(yīng)線程獲取讀鎖的次數(shù),當(dāng)需要判斷某個線程能否獲得讀鎖時,就利用map中存儲的數(shù)據(jù)進(jìn)行判斷。下面是方法lockRead和unlockRead修改后的的代碼:
public class ReadWriteLock{ private MapreadingThreads = new HashMap (); private int writers = 0; private int writeRequests = 0; public synchronized void lockRead() throws InterruptedException{ Thread callingThread = Thread.currentThread(); while(! canGrantReadAccess(callingThread)){ wait(); } readingThreads.put(callingThread, (getAccessCount(callingThread) + 1)); } public synchronized void unlockRead(){ Thread callingThread = Thread.currentThread(); int accessCount = getAccessCount(callingThread); if(accessCount == 1) { readingThreads.remove(callingThread); } else { readingThreads.put(callingThread, (accessCount -1)); } notifyAll(); } private boolean canGrantReadAccess(Thread callingThread){ if(writers > 0) return false; if(isReader(callingThread) return true; if(writeRequests > 0) return false; return true; } private int getReadAccessCount(Thread callingThread){ Integer accessCount = readingThreads.get(callingThread); if(accessCount == null) return 0; return accessCount.intValue(); } private boolean isReader(Thread callingThread){ return readingThreads.get(callingThread) != null; } }
代碼中我們可以看到,只有在沒有線程擁有寫鎖的情況下才允許讀鎖的重入。此外,重入的讀鎖比寫鎖優(yōu)先級高。
寫鎖重入僅當(dāng)一個線程已經(jīng)持有寫鎖,才允許寫鎖重入(再次獲得寫鎖)。下面是方法lockWrite和unlockWrite修改后的的代碼。
public class ReadWriteLock{ private MapreadingThreads = new HashMap (); private int writeAccesses = 0; private int writeRequests = 0; private Thread writingThread = null; public synchronized void lockWrite() throws InterruptedException{ writeRequests++; Thread callingThread = Thread.currentThread(); while(!canGrantWriteAccess(callingThread)){ wait(); } writeRequests--; writeAccesses++; writingThread = callingThread; } public synchronized void unlockWrite() throws InterruptedException{ writeAccesses--; if(writeAccesses == 0){ writingThread = null; } notifyAll(); } private boolean canGrantWriteAccess(Thread callingThread){ if(hasReaders()) return false; if(writingThread == null) return true; if(!isWriter(callingThread)) return false; return true; } private boolean hasReaders(){ return readingThreads.size() > 0; } private boolean isWriter(Thread callingThread){ return writingThread == callingThread; } }
注意在確定當(dāng)前線程是否能夠獲取寫鎖的時候,是如何處理的。
讀鎖升級到寫鎖有時,我們希望一個擁有讀鎖的線程,也能獲得寫鎖。想要允許這樣的操作,要求這個線程是唯一一個擁有讀鎖的線程。writeLock()需要做點(diǎn)改動來達(dá)到這個目的:
public class ReadWriteLock{ private MapreadingThreads = new HashMap (); private int writeAccesses = 0; private int writeRequests = 0; private Thread writingThread = null; public synchronized void lockWrite() throws InterruptedException{ writeRequests++; Thread callingThread = Thread.currentThread(); while(!canGrantWriteAccess(callingThread)){ wait(); } writeRequests--; writeAccesses++; writingThread = callingThread; } public synchronized void unlockWrite() throws InterruptedException{ writeAccesses--; if(writeAccesses == 0){ writingThread = null; } notifyAll(); } private boolean canGrantWriteAccess(Thread callingThread){ if(isOnlyReader(callingThread)) return true; if(hasReaders()) return false; if(writingThread == null) return true; if(!isWriter(callingThread)) return false; return true; } private boolean hasReaders(){ return readingThreads.size() > 0; } private boolean isWriter(Thread callingThread){ return writingThread == callingThread; } private boolean isOnlyReader(Thread thread){ return readers == 1 && readingThreads.get(callingThread) != null; } }
現(xiàn)在ReadWriteLock類就可以從讀鎖升級到寫鎖了。
寫鎖降級到讀鎖有時擁有寫鎖的線程也希望得到讀鎖。如果一個線程擁有了寫鎖,那么自然其它線程是不可能擁有讀鎖或?qū)戞i了。所以對于一個擁有寫鎖的線程,再獲得讀鎖,是不會有什么危險的。我們僅僅需要對上面canGrantReadAccess方法進(jìn)行簡單地修改:
public class ReadWriteLock{ private boolean canGrantReadAccess(Thread callingThread){ if(isWriter(callingThread)) return true; if(writingThread != null) return false; if(isReader(callingThread) return true; if(writeRequests > 0) return false; return true; } }可重入的ReadWriteLock的完整實(shí)現(xiàn)
下面是完整的ReadWriteLock實(shí)現(xiàn)。為了便于代碼的閱讀與理解,簡單對上面的代碼做了重構(gòu)。重構(gòu)后的代碼如下。
public class ReadWriteLock{ private Map在finally中調(diào)用unlock()readingThreads = new HashMap (); private int writeAccesses = 0; private int writeRequests = 0; private Thread writingThread = null; public synchronized void lockRead() throws InterruptedException{ Thread callingThread = Thread.currentThread(); while(! canGrantReadAccess(callingThread)){ wait(); } readingThreads.put(callingThread, (getReadAccessCount(callingThread) + 1)); } private boolean canGrantReadAccess(Thread callingThread){ if(isWriter(callingThread)) return true; if(hasWriter()) return false; if(isReader(callingThread)) return true; if(hasWriteRequests()) return false; return true; } public synchronized void unlockRead(){ Thread callingThread = Thread.currentThread(); if(!isReader(callingThread)){ throw new IllegalMonitorStateException( "Calling Thread does not" + " hold a read lock on this ReadWriteLock"); } int accessCount = getReadAccessCount(callingThread); if(accessCount == 1){ readingThreads.remove(callingThread); } else { readingThreads.put(callingThread, (accessCount -1)); } notifyAll(); } public synchronized void lockWrite() throws InterruptedException{ writeRequests++; Thread callingThread = Thread.currentThread(); while(!canGrantWriteAccess(callingThread)){ wait(); } writeRequests--; writeAccesses++; writingThread = callingThread; } public synchronized void unlockWrite() throws InterruptedException{ if(!isWriter(Thread.currentThread()){ throw new IllegalMonitorStateException( "Calling Thread does not" + " hold the write lock on this ReadWriteLock"); } writeAccesses--; if(writeAccesses == 0){ writingThread = null; } notifyAll(); } private boolean canGrantWriteAccess(Thread callingThread){ if(isOnlyReader(callingThread)) return true; if(hasReaders()) return false; if(writingThread == null) return true; if(!isWriter(callingThread)) return false; return true; } private int getReadAccessCount(Thread callingThread){ Integer accessCount = readingThreads.get(callingThread); if(accessCount == null) return 0; return accessCount.intValue(); } private boolean hasReaders(){ return readingThreads.size() > 0; } private boolean isReader(Thread callingThread){ return readingThreads.get(callingThread) != null; } private boolean isOnlyReader(Thread callingThread){ return readingThreads.size() == 1 && readingThreads.get(callingThread) != null; } private boolean hasWriter(){ return writingThread != null; } private boolean isWriter(Thread callingThread){ return writingThread == callingThread; } private boolean hasWriteRequests(){ return this.writeRequests > 0; } }
在利用ReadWriteLock來保護(hù)臨界區(qū)時,如果臨界區(qū)可能拋出異常,在finally塊中調(diào)用readUnlock()和writeUnlock()就顯得很重要了。這樣做是為了保證ReadWriteLock能被成功解鎖,然后其它線程可以請求到該鎖。這里有個例子:
lock.lockWrite(); try{ //do critical section code, which may throw exception } finally { lock.unlockWrite(); }
上面這樣的代碼結(jié)構(gòu)能夠保證臨界區(qū)中拋出異常時ReadWriteLock也會被釋放。如果unlockWrite方法不是在finally塊中調(diào)用的,當(dāng)臨界區(qū)拋出了異常時,ReadWriteLock
會一直保持在寫鎖定狀態(tài),就會導(dǎo)致所有調(diào)用lockRead()或lockWrite()的線程一直阻塞。唯一能夠重新解鎖ReadWriteLock的因素可能就是ReadWriteLock是可重入的,當(dāng)拋出異常時,這個線程后續(xù)還可以成功獲取這把鎖,然后執(zhí)行臨界區(qū)以及再次調(diào)用unlockWrite(),這就會再次釋放ReadWriteLock。但是如果該線程后續(xù)不再獲取這把鎖了呢?所以,在finally中調(diào)用unlockWrite對寫出健壯代碼是很重要的。
原文 Read Write Locks
作者 Jakob Jenkov
譯者 微涼
校對 丁一
via ifeve
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/69766.html
摘要:當(dāng)前線程在超時時間內(nèi)被中斷超時時間結(jié)束,返回釋放鎖獲取等待通知組件,該組件和當(dāng)前的鎖綁定,當(dāng)前線程只有獲取了鎖,才能調(diào)用該組件的方法,調(diào)用后,當(dāng)前線程將釋放鎖。同步器是實(shí)現(xiàn)鎖的關(guān)鍵,在鎖的實(shí)現(xiàn)中聚合同步器,利用同步器實(shí)現(xiàn)鎖的語義。 本文在參考java并發(fā)編程實(shí)戰(zhàn)后完成,參考內(nèi)容較多 Java中的鎖 鎖是用來控制多線程訪問共享資源的方式,一個鎖能夠防止多個線程同事訪問共享資源。在Lock...
摘要:有可能,會造成優(yōu)先級反轉(zhuǎn)或者饑餓現(xiàn)象。悲觀鎖在中的使用,就是利用各種鎖。對于而言,其是獨(dú)享鎖。偏向鎖,顧名思義,它會偏向于第一個訪問鎖的線程,大多數(shù)情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。 理解鎖的基礎(chǔ)知識 如果想要透徹的理解java鎖的來龍去脈,需要先了解以下基礎(chǔ)知識。 基礎(chǔ)知識之一:鎖的類型 按照其性質(zhì)分類 公平鎖/非公平鎖 公平鎖是指多個線程按照申請鎖的順序來獲...
摘要:公平鎖非公平鎖公平鎖公平鎖是指多個線程按照申請鎖的順序來獲取鎖。加鎖后,任何其他試圖再次加鎖的線程會被阻塞,直到當(dāng)前進(jìn)程解鎖。重量級鎖會讓其他申請的線程進(jìn)入阻塞,性能降低。 Java 中15種鎖的介紹 在讀很多并發(fā)文章中,會提及各種各樣鎖如公平鎖,樂觀鎖等等,這篇文章介紹各種鎖的分類。介紹的內(nèi)容如下: 公平鎖 / 非公平鎖 可重入鎖 / 不可重入鎖 獨(dú)享鎖 / 共享鎖 互斥鎖 / 讀...
摘要:此時線程和會再有一個線程能夠獲取寫鎖,假設(shè)是,如果不采用再次驗(yàn)證的方式,此時會再次查詢數(shù)據(jù)庫。而實(shí)際上線程已經(jīng)把緩存的值設(shè)置好了,完全沒有必要再次查詢數(shù)據(jù)庫。 大家知道了Java中使用管程同步原語,理論上可以解決所有的并發(fā)問題。那 Java SDK 并發(fā)包里為什么還有很多其他的工具類呢?原因很簡單:分場景優(yōu)化性能,提升易用性 今天我們就介紹一種非常普遍的并發(fā)場景:讀多寫少場景。實(shí)際工作...
摘要:關(guān)于,最后有兩點(diǎn)規(guī)律需要注意當(dāng)?shù)牡却?duì)列隊(duì)首結(jié)點(diǎn)是共享結(jié)點(diǎn),說明當(dāng)前寫鎖被占用,當(dāng)寫鎖釋放時,會以傳播的方式喚醒頭結(jié)點(diǎn)之后緊鄰的各個共享結(jié)點(diǎn)。當(dāng)?shù)牡却?duì)列隊(duì)首結(jié)點(diǎn)是獨(dú)占結(jié)點(diǎn),說明當(dāng)前讀鎖被使用,當(dāng)讀鎖釋放歸零后,會喚醒隊(duì)首的獨(dú)占結(jié)點(diǎn)。 showImg(https://segmentfault.com/img/remote/1460000016012293); 本文首發(fā)于一世流云的專欄:...
摘要:如何維護(hù)狀態(tài)內(nèi)部維護(hù)的讀寫狀態(tài)是由位碼表示,高位為讀狀態(tài),表示持有讀鎖的線程數(shù),低位為寫狀態(tài),表示寫鎖的重入次數(shù),狀態(tài)的改變通過實(shí)現(xiàn),保證同步。寫鎖降級到讀鎖有時擁有寫鎖的線程也希望得到讀鎖。 ReentrantReadWriteLock 如何保證同步 Java中的可重入讀寫鎖ReentrantReadWriteLock是基于AQS(AbstractQueuedSynchronizer...
閱讀 3061·2021-11-23 09:51
閱讀 1040·2021-09-02 15:21
閱讀 3005·2019-08-30 13:56
閱讀 1829·2019-08-29 14:12
閱讀 708·2019-08-29 13:53
閱讀 1664·2019-08-29 11:32
閱讀 1325·2019-08-29 11:25
閱讀 1493·2019-08-28 17:51