閱讀546 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Java中的讀/寫鎖

相比Java中的鎖(Locks in Java)裏 Lock實現,讀寫鎖更複雜一些。假設你的程序中涉及到對一些共享資源的讀和寫操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,兩個線程同時讀一 個資源沒有任何問題,所以應該允許多個線程能在同時讀取共享資源。但是如果有一個線程想去寫這些共享資源,就不應該再有其它線程對該資源進行讀或寫(譯者注:也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。這就需要一個讀/寫鎖來解決這個問題。

Java5在java.util.concurrent包中已經包含了讀寫鎖。盡管如此,我們還是應該了解其實現背後的原理。

以下是本文的主題

  1. 讀/寫鎖的Java實現(Read / Write Lock Java Implementation)
  2. 讀/寫鎖的重入(Read / Write Lock Reentrance)
  3. 讀鎖重入(Read Reentrance)
  4. 寫鎖重入(Write Reentrance)
  5. 讀鎖升級到寫鎖(Read to Write Reentrance)
  6. 寫鎖降級到讀鎖(Write to Read Reentrance)
  7. 可重入的ReadWriteLock的完整實現(Fully Reentrant ReadWriteLock)
  8. 在finally中調用unlock() (Calling unlock() from a finally-clause)

讀/寫鎖的Java實現

先讓我們對讀寫訪問資源的條件做個概述:

讀取 沒有線程正在做寫操作,且沒有線程在請求寫操作。

寫入 沒有線程正在做讀寫操作。

如果某個線程想要讀取資源,隻要沒有線程正在對該資源進行寫操作且沒有線程請求對該資源的寫操作即可。我們假設對寫操作的請求比對讀操作的請求更重要,就 要提升寫請求的優先級。此外,如果讀操作發生的比較頻繁,我們又沒有提升寫操作的優先級,那麼就會產生“饑餓”現象。請求寫操作的線程會一直阻塞,直到所 有的讀線程都從ReadWriteLock上解鎖了。如果一直保證新線程的讀操作權限,那麼等待寫操作的線程就會一直阻塞下去,結果就是發生“饑餓”。因 此,隻有當沒有線程正在鎖住ReadWriteLock進行寫操作,且沒有線程請求該鎖準備執行寫操作時,才能保證讀操作繼續。

當其它線程沒有對共享資源進行讀操作或者寫操作時,某個線程就有可能獲得該共享資源的寫鎖,進而對共享資源進行寫操作。有多少線程請求了寫鎖以及以何種順序請求寫鎖並不重要,除非你想保證寫鎖請求的公平性。

按照上麵的敘述,簡單的實現出一個讀/寫鎖,代碼如下

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類中,讀鎖和寫鎖各有一個獲取鎖和釋放鎖的方法。

讀鎖的實現在lockRead()中,隻要沒有線程擁有寫鎖(writers==0),且沒有線程在請求寫鎖(writeRequests ==0),所有想獲得讀鎖的線程都能成功獲取。

寫鎖的實現在lockWrite()中,當一個線程想獲得寫鎖的時候,首先會把寫鎖請求數加1(writeRequests++),然後再去判斷是否能夠 真能獲得寫鎖,當沒有線程持有讀鎖(readers==0 ),且沒有線程持有寫鎖(writers==0)時就能獲得寫鎖。有多少線程在請求寫鎖並無關係。

需要注意的是,在兩個釋放鎖的方法(unlockRead,unlockWrite)中,都調用了notifyAll方法,而不是notify。要解釋這個原因,我們可以想象下麵一種情形:

如果有線程在等待獲取讀鎖,同時又有線程在等待獲取寫鎖。如果這時其中一個等待讀鎖的線程被notify方法喚醒,但因為此時仍有請求寫鎖的線程存在 (writeRequests>0),所以被喚醒的線程會再次進入阻塞狀態。然而,等待寫鎖的線程一個也沒被喚醒,就像什麼也沒發生過一樣(譯者注:信號丟失現象)。如果用的是notifyAll方法,所有的線程都會被喚醒,然後判斷能否獲得其請求的鎖。

用notifyAll還有一個好處。如果有多個讀線程在等待讀鎖且沒有線程在等待寫鎖時,調用unlockWrite()後,所有等待讀鎖的線程都能立馬成功獲取讀鎖 —— 而不是一次隻允許一個。

讀/寫鎖的重入

上麵實現的讀/寫鎖(ReadWriteLock) 是不可重入的,當一個已經持有寫鎖的線程再次請求寫鎖時,就會被阻塞。原因是已經有一個寫線程了——就是它自己。此外,考慮下麵的例子:

  1. Thread 1 獲得了讀鎖
  2. Thread 2 請求寫鎖,但因為Thread 1 持有了讀鎖,所以寫鎖請求被阻塞。
  3. Thread 1 再想請求一次讀鎖,但因為Thread 2處於請求寫鎖的狀態,所以想再次獲取讀鎖也會被阻塞。

上麵這種情形使用前麵的ReadWriteLock就會被鎖定——一種類似於死鎖的情形。不會再有線程能夠成功獲取讀鎖或寫鎖了。

為了讓ReadWriteLock可重入,需要對它做一些改進。下麵會分別處理讀鎖的重入和寫鎖的重入。

讀鎖重入

為了讓ReadWriteLock的讀鎖可重入,我們要先為讀鎖重入建立規則:

  • 要保證某個線程中的讀鎖可重入,要麼滿足獲取讀鎖的條件(沒有寫或寫請求),要麼已經持有讀鎖(不管是否有寫請求)。

要確定一個線程是否已經持有讀鎖,可以用一個map來存儲已經持有讀鎖的線程以及對應線程獲取讀鎖的次數,當需要判斷某個線程能否獲得讀鎖時,就利用map中存儲的數據進行判斷。下麵是方法lockRead和unlockRead修改後的的代碼:

public class ReadWriteLock{
	private Map<Thread, Integer> readingThreads =
		new HashMap<Thread, Integer>();

	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;
	}
}

代碼中我們可以看到,隻有在沒有線程擁有寫鎖的情況下才允許讀鎖的重入。此外,重入的讀鎖比寫鎖優先級高。

寫鎖重入

僅當一個線程已經持有寫鎖,才允許寫鎖重入(再次獲得寫鎖)。下麵是方法lockWrite和unlockWrite修改後的的代碼。

public class ReadWriteLock{
	private Map<Thread, Integer> readingThreads =
		new HashMap<Thread, Integer>();

	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;
	}
}

注意在確定當前線程是否能夠獲取寫鎖的時候,是如何處理的。

讀鎖升級到寫鎖

有時,我們希望一個擁有讀鎖的線程,也能獲得寫鎖。想要允許這樣的操作,要求這個線程是唯一一個擁有讀鎖的線程。writeLock()需要做點改動來達到這個目的:

public class ReadWriteLock{
	private Map<Thread, Integer> readingThreads =
		new HashMap<Thread, Integer>();

	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;
	}
}

現在ReadWriteLock類就可以從讀鎖升級到寫鎖了。

寫鎖降級到讀鎖

有時擁有寫鎖的線程也希望得到讀鎖。如果一個線程擁有了寫鎖,那麼自然其它線程是不可能擁有讀鎖或寫鎖了。所以對於一個擁有寫鎖的線程,再獲得讀鎖,是不會有什麼危險的。我們僅僅需要對上麵canGrantReadAccess方法進行簡單地修改:

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的完整實現

下麵是完整的ReadWriteLock實現。為了便於代碼的閱讀與理解,簡單對上麵的代碼做了重構。重構後的代碼如下。

public class ReadWriteLock{
	private Map<Thread, Integer> readingThreads =
		new HashMap<Thread, Integer>();

	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;
	}
}

在finally中調用unlock()

在利用ReadWriteLock來保護臨界區時,如果臨界區可能拋出異常,在finally塊中調用readUnlock()和 writeUnlock()就顯得很重要了。這樣做是為了保證ReadWriteLock能被成功解鎖,然後其它線程可以請求到該鎖。這裏有個例子:

lock.lockWrite();
try{
	//do critical section code, which may throw exception
} finally {
	lock.unlockWrite();
}

上麵這樣的代碼結構能夠保證臨界區中拋出異常時ReadWriteLock也會被釋放。如果unlockWrite方法不是在finally塊中調用的, 當臨界區拋出了異常時,ReadWriteLock 會一直保持在寫鎖定狀態,就會導致所有調用lockRead()或lockWrite()的線程一直阻塞。唯一能夠重新解鎖ReadWriteLock的 因素可能就是ReadWriteLock是可重入的,當拋出異常時,這個線程後續還可以成功獲取這把鎖,然後執行臨界區以及再次調用 unlockWrite(),這就會再次釋放ReadWriteLock。但是如果該線程後續不再獲取這把鎖了呢?所以,在finally中調用 unlockWrite對寫出健壯代碼是很重要的。


文章轉自 並發編程網-ifeve.com

最後更新:2017-05-22 20:04:53

  上一篇:go  暢捷通入駐阿裏雲市場,為小微企業在線“號脈開方”
  下一篇:go  Java並發包中的同步隊列SynchronousQueue實現原理