Java中的讀/寫鎖
原文連結 作者:Jakob Jenkov 譯者:微涼 校對:丁一
相比Java中的鎖(Locks in Java)裡Lock實現,讀寫鎖更復雜一些。假設你的程式中涉及到對一些共享資源的讀和寫操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,兩個執行緒同時讀一個資源沒有任何問題,所以應該允許多個執行緒能在同時讀取共享資源。但是如果有一個執行緒想去寫這些共享資源,就不應該再有其它執行緒對該資源進行讀或寫(譯者注:也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。這就需要一個讀/寫鎖來解決這個問題。
Java5在java.util.concurrent包中已經包含了讀寫鎖。儘管如此,我們還是應該瞭解其實現背後的原理。
以下是本文的主題
讀/寫鎖的Java實現(Read / Write Lock Java Implementation)
讀/寫鎖的重入(Read / Write Lock Reentrance)
讀鎖升級到寫鎖(Read to Write Reentrance)
寫鎖降級到讀鎖(Write to Read Reentrance)
可重入的ReadWriteLock的完整實現(Fully Reentrant ReadWriteLock)
在finally中呼叫unlock() (Calling unlock() from a finally-clause)
讀/寫鎖的Java實現
先讓我們對讀寫訪問資源的條件做個概述:
讀取 沒有執行緒正在做寫操作,且沒有執行緒在請求寫操作。
寫入 沒有執行緒正在做讀寫操作。
如果某個執行緒想要讀取資源,只要沒有執行緒正在對該資源進行寫操作且沒有執行緒請求對該資源的寫操作即可。我們假設對寫操作的請求比對讀操作的請求更重要,就要提升寫請求的優先順序。此外,如果讀操作發生的比較頻繁,我們又沒有提升寫操作的優先順序,那麼就會產生“飢餓”現象。請求寫操作的執行緒會一直阻塞,直到所有的讀執行緒都從ReadWriteLock上解鎖了。如果一直保證新執行緒的讀操作許可權,那麼等待寫操作的執行緒就會一直阻塞下去,結果就是發生“飢餓”。因此,只有當沒有執行緒正在鎖住ReadWriteLock進行寫操作,且沒有執行緒請求該鎖準備執行寫操作時,才能保證讀操作繼續。
當其它執行緒沒有對共享資源進行讀操作或者寫操作時,某個執行緒就有可能獲得該共享資源的寫鎖,進而對共享資源進行寫操作。有多少執行緒請求了寫鎖以及以何種順序請求寫鎖並不重要,除非你想保證寫鎖請求的公平性。
按照上面的敘述,簡單的實現出一個讀/寫鎖,程式碼如下
01public class ReadWriteLock{
02 private int readers = 0;
03 private int writers = 0;
04 private int writeRequests = 0;
05
06 public synchronized void lockRead()
07 throws InterruptedException{
08 while(writers > 0 || writeRequests > 0){
09 wait();
10 }
11 readers++;
12 }
13
14 public synchronized void unlockRead(){
15 readers--;
16 notifyAll();
17 }
18
19 public synchronized void lockWrite()
20 throws InterruptedException{
21 writeRequests++;
22
23 while(readers > 0 || writers > 0){
24 wait();
25 }
26 writeRequests--;
27 writers++;
28 }
29
30 public synchronized void unlockWrite()
31 throws InterruptedException{
32 writers--;
33 notifyAll();
34 }
35}
ReadWriteLock類中,讀鎖和寫鎖各有一個獲取鎖和釋放鎖的方法。
讀鎖的實現在lockRead()中,只要沒有執行緒擁有寫鎖(writers==0),且沒有執行緒在請求寫鎖(writeRequests ==0),所有想獲得讀鎖的執行緒都能成功獲取。
寫鎖的實現在lockWrite()中,當一個執行緒想獲得寫鎖的時候,首先會把寫鎖請求數加1(writeRequests++),然後再去判斷是否能夠真能獲得寫鎖,當沒有執行緒持有讀鎖(readers==0 ),且沒有執行緒持有寫鎖(writers==0)時就能獲得寫鎖。有多少執行緒在請求寫鎖並無關係。
需要注意的是,在兩個釋放鎖的方法(unlockRead,unlockWrite)中,都呼叫了notifyAll方法,而不是notify。要解釋這個原因,我們可以想象下面一種情形:
如果有執行緒在等待獲取讀鎖,同時又有執行緒在等待獲取寫鎖。如果這時其中一個等待讀鎖的執行緒被notify方法喚醒,但因為此時仍有請求寫鎖的執行緒存在(writeRequests>0),所以被喚醒的執行緒會再次進入阻塞狀態。然而,等待寫鎖的執行緒一個也沒被喚醒,就像什麼也沒發生過一樣(譯者注:訊號丟失現象)。如果用的是notifyAll方法,所有的執行緒都會被喚醒,然後判斷能否獲得其請求的鎖。
用notifyAll還有一個好處。如果有多個讀執行緒在等待讀鎖且沒有執行緒在等待寫鎖時,呼叫unlockWrite()後,所有等待讀鎖的執行緒都能立馬成功獲取讀鎖 —— 而不是一次只允許一個。
讀/寫鎖的重入
上面實現的讀/寫鎖(ReadWriteLock) 是不可重入的,當一個已經持有寫鎖的執行緒再次請求寫鎖時,就會被阻塞。原因是已經有一個寫執行緒了——就是它自己。此外,考慮下面的例子:
Thread 1 獲得了讀鎖
Thread 2 請求寫鎖,但因為Thread 1 持有了讀鎖,所以寫鎖請求被阻塞。
Thread 1 再想請求一次讀鎖,但因為Thread 2處於請求寫鎖的狀態,所以想再次獲取讀鎖也會被阻塞。
上面這種情形使用前面的ReadWriteLock就會被鎖定——一種類似於死鎖的情形。不會再有執行緒能夠成功獲取讀鎖或寫鎖了。
為了讓ReadWriteLock可重入,需要對它做一些改進。下面會分別處理讀鎖的重入和寫鎖的重入。
讀鎖重入
為了讓ReadWriteLock的讀鎖可重入,我們要先為讀鎖重入建立規則:
要保證某個執行緒中的讀鎖可重入,要麼滿足獲取讀鎖的條件(沒有寫或寫請求),要麼已經持有讀鎖(不管是否有寫請求)。
要確定一個執行緒是否已經持有讀鎖,可以用一個map來儲存已經持有讀鎖的執行緒以及對應執行緒獲取讀鎖的次數,當需要判斷某個執行緒能否獲得讀鎖時,就利用map中儲存的資料進行判斷。下面是方法lockRead和unlockRead修改後的的程式碼:
01public class ReadWriteLock{
02 private Map readingThreads =
03 new HashMap();
04
05 private int writers = 0;
06 private int writeRequests = 0;
07
08 public synchronized void lockRead()
09 throws InterruptedException{
10 Thread callingThread = Thread.currentThread();
11 while(! canGrantReadAccess(callingThread)){
12 wait();
13 }
14
15 readingThreads.put(callingThread,
16 (getAccessCount(callingThread) + 1));
17 }
18
19 public synchronized void unlockRead(){
20 Thread callingThread = Thread.currentThread();
21 int accessCount = getAccessCount(callingThread);
22 if(accessCount == 1) {
23 readingThreads.remove(callingThread);
24 } else {
25 readingThreads.put(callingThread, (accessCount -1));
26 }
27 notifyAll();
28 }
29
30 private boolean canGrantReadAccess(Thread callingThread){
31 if(writers > 0) return false;
32 if(isReader(callingThread) return true;
33 if(writeRequests > 0) return false;
34 return true;
35 }
36
37 private int getReadAccessCount(Thread callingThread){
38 Integer accessCount = readingThreads.get(callingThread);
39 if(accessCount == null) return 0;
40 return accessCount.intValue();
41 }
42
43 private boolean isReader(Thread callingThread){
44 return readingThreads.get(callingThread) != null;
45 }
46}
程式碼中我們可以看到,只有在沒有執行緒擁有寫鎖的情況下才允許讀鎖的重入。此外,重入的讀鎖比寫鎖優先順序高。
寫鎖重入
僅當一個執行緒已經持有寫鎖,才允許寫鎖重入(再次獲得寫鎖)。下面是方法lockWrite和unlockWrite修改後的的程式碼。
01public class ReadWriteLock{
02 private Map readingThreads =
03 new HashMap();
04
05 private int writeAccesses = 0;
06 private int writeRequests = 0;
07 private Thread writingThread = null;
08
09 public synchronized void lockWrite()
10 throws InterruptedException{
11 writeRequests++;
12 Thread callingThread = Thread.currentThread();
13 while(!canGrantWriteAccess(callingThread)){
14 wait();
15 }
16 writeRequests--;
17 writeAccesses++;
18 writingThread = callingThread;
19 }
20
21 public synchronized void unlockWrite()
22 throws InterruptedException{
23 writeAccesses--;
24 if(writeAccesses == 0){
25 writingThread = null;
26 }
27 notifyAll();
28 }
29
30 private boolean canGrantWriteAccess(Thread callingThread){
31 if(hasReaders()) return false;
32 if(writingThread == null) return true;
33 if(!isWriter(callingThread)) return false;
34 return true;
35 }
36
37 private boolean hasReaders(){
38 return readingThreads.size() > 0;
39 }
40
41 private boolean isWriter(Thread callingThread){
42 return writingThread == callingThread;
43 }
44}
注意在確定當前執行緒是否能夠獲取寫鎖的時候,是如何處理的。
讀鎖升級到寫鎖
有時,我們希望一個擁有讀鎖的執行緒,也能獲得寫鎖。想要允許這樣的操作,要求這個執行緒是唯一一個擁有讀鎖的執行緒。writeLock()需要做點改動來達到這個目的:
01public class ReadWriteLock{
02 private Map readingThreads =
03 new HashMap();
04
05 private int writeAccesses = 0;
06 private int writeRequests = 0;
07 private Thread writingThread = null;
08
09 public synchronized void lockWrite()
10 throws InterruptedException{
11 writeRequests++;
12 Thread callingThread = Thread.currentThread();
13 while(!canGrantWriteAccess(callingThread)){
14 wait();
15 }
16 writeRequests--;
17 writeAccesses++;
18 writingThread = callingThread;
19 }
20
21 public synchronized void unlockWrite() throws InterruptedException{
22 writeAccesses--;
23 if(writeAccesses == 0){
24 writingThread = null;
25 }
26 notifyAll();
27 }
28
29 private boolean canGrantWriteAccess(Thread callingThread){
30 if(isOnlyReader(callingThread)) return true;
31 if(hasReaders()) return false;
32 if(writingThread == null) return true;
33 if(!isWriter(callingThread)) return false;
34 return true;
35 }
36
37 private boolean hasReaders(){
38 return readingThreads.size() > 0;
39 }
40
41 private boolean isWriter(Thread callingThread){
42 return writingThread == callingThread;
43 }
44
45 private boolean isOnlyReader(Thread thread){
46 return readers == 1 && readingThreads.get(callingThread) != null;
47 }
48}
現在ReadWriteLock類就可以從讀鎖升級到寫鎖了。
寫鎖降級到讀鎖
有時擁有寫鎖的執行緒也希望得到讀鎖。如果一個執行緒擁有了寫鎖,那麼自然其它執行緒是不可能擁有讀鎖或寫鎖了。所以對於一個擁有寫鎖的執行緒,再獲得讀鎖,是不會有什麼危險的。我們僅僅需要對上面canGrantReadAccess方法進行簡單地修改:
1public class ReadWriteLock{
2 private boolean canGrantReadAccess(Thread callingThread){
3 if(isWriter(callingThread)) return true;
4 if(writingThread != null) return false;
5 if(isReader(callingThread) return true;
6 if(writeRequests > 0) return false;
7 return true;
8 }
9}
可重入的ReadWriteLock的完整實現
下面是完整的ReadWriteLock實現。為了便於程式碼的閱讀與理解,簡單對上面的程式碼做了重構。重構後的程式碼如下。
001public class ReadWriteLock{
002 private Map readingThreads =
003 new HashMap();
004
005 private int writeAccesses = 0;
006 private int writeRequests = 0;
007 private Thread writingThread = null;
008
009 public synchronized void lockRead()
010 throws InterruptedException{
011 Thread callingThread = Thread.currentThread();
012 while(! canGrantReadAccess(callingThread)){
013 wait();
014 }
015
016 readingThreads.put(callingThread,
017 (getReadAccessCount(callingThread) + 1));
018 }
019
020 private boolean canGrantReadAccess(Thread callingThread){
021 if(isWriter(callingThread)) return true;
022 if(hasWriter()) return false;
023 if(isReader(callingThread)) return true;
024 if(hasWriteRequests()) return false;
025 return true;
026 }
027
028
029 public synchronized void unlockRead(){
030 Thread callingThread = Thread.currentThread();
031 if(!isReader(callingThread)){
032 throw new IllegalMonitorStateException(
033 "Calling Thread does not" +
034 " hold a read lock on this ReadWriteLock");
035 }
036 int accessCount = getReadAccessCount(callingThread);
037 if(accessCount == 1){
038 readingThreads.remove(callingThread);
039 } else {
040 readingThreads.put(callingThread, (accessCount -1));
041 }
042 notifyAll();
043 }
044
045 public synchronized void lockWrite()
046 throws InterruptedException{
047 writeRequests++;
048 Thread callingThread = Thread.currentThread();
049 while(!canGrantWriteAccess(callingThread)){
050 wait();
051 }
052 writeRequests--;
053 writeAccesses++;
054 writingThread = callingThread;
055 }
056
057 public synchronized void unlockWrite()
058 throws InterruptedException{
059 if(!isWriter(Thread.currentThread()){
060 throw new IllegalMonitorStateException(
061 "Calling Thread does not" +
062 " hold the write lock on this ReadWriteLock");
063 }
064 writeAccesses--;
065 if(writeAccesses == 0){
066 writingThread = null;
067 }
068 notifyAll();
069 }
070
071 private boolean canGrantWriteAccess(Thread callingThread){
072 if(isOnlyReader(callingThread)) return true;
073 if(hasReaders()) return false;
074 if(writingThread == null) return true;
075 if(!isWriter(callingThread)) return false;
076 return true;
077 }
078
079
080 private int getReadAccessCount(Thread callingThread){
081 Integer accessCount = readingThreads.get(callingThread);
082 if(accessCount == null) return 0;
083 return accessCount.intValue();
084 }
085
086
087 private boolean hasReaders(){
088 return readingThreads.size() > 0;
089 }
090
091 private boolean isReader(Thread callingThread){
092 return readingThreads.get(callingThread) != null;
093 }
094
095 private boolean isOnlyReader(Thread callingThread){
096 return readingThreads.size() == 1 &&
097 readingThreads.get(callingThread) != null;
098 }
099
100 private boolean hasWriter(){
101 return writingThread != null;
102 }
103
104 private boolean isWriter(Thread callingThread){
105 return writingThread == callingThread;
106 }
107
108 private boolean hasWriteRequests(){
109 return this.writeRequests > 0;
110 }
111}
在finally中呼叫unlock()
在利用ReadWriteLock來保護臨界區時,如果臨界區可能丟擲異常,在finally塊中呼叫readUnlock()和writeUnlock()就顯得很重要了。這樣做是為了保證ReadWriteLock能被成功解鎖,然後其它執行緒可以請求到該鎖。這裡有個例子:
1lock.lockWrite();
2try{
3 //do critical section code, which may throw exception
4} finally {
5 lock.unlockWrite();
6}
上面這樣的程式碼結構能夠保證臨界區中丟擲異常時ReadWriteLock也會被釋放。如果unlockWrite方法不是在finally塊中呼叫的,當臨界區丟擲了異常時,ReadWriteLock 會一直保持在寫鎖定狀態,就會導致所有呼叫lockRead()或lockWrite()的執行緒一直阻塞。唯一能夠重新解鎖ReadWriteLock的因素可能就是ReadWriteLock是可重入的,當丟擲異常時,這個執行緒後續還可以成功獲取這把鎖,然後執行臨界區以及再次呼叫unlockWrite(),這就會再次釋放ReadWriteLock。但是如果該執行緒後續不再獲取這把鎖了呢?所以,在finally中呼叫unlockWrite對寫出健壯程式碼是很重要的。
原創文章,轉載請註明: 轉載自併發程式設計網 – ifeve.com本文連結地址: Java中的讀/寫鎖
相關文章
- Concurrency(十五: Java中的讀寫鎖)Java
- 淺談Java中的鎖:Synchronized、重入鎖、讀寫鎖Javasynchronized
- Java讀寫鎖ReadWriteLockJava
- Java併發(8)- 讀寫鎖中的效能之王:StampedLockJava
- c++中的讀寫鎖C++
- Java併發——讀寫鎖ReentrantReadWriteLockJava
- Java中的讀寫鎖ReentrantReadWriteLock詳解,存在一個小缺陷Java
- Java 讀寫鎖 ReentrantReadWriteLock 原始碼分析Java原始碼
- Java併發-顯式鎖篇【可重入鎖+讀寫鎖】Java
- 讀寫鎖
- ReentrantReadWriteLock讀寫鎖及其在 RxCache 中的使用
- 讀寫鎖 ReentrantReadWriteLock 與 互斥鎖 的效率
- MySQL MyISAM引擎的讀鎖與寫鎖MySql
- 讀寫鎖 ReentrantReadWriteLock
- Java併發程式設計-讀寫鎖(ReentrantReadWriteLock)Java程式設計
- Java併發指南10:Java 讀寫鎖 ReentrantReadWriteLock 原始碼分析Java原始碼
- Lock鎖之重入鎖與讀寫鎖
- Java併發程式設計之鎖機制之ReentrantReadWriteLock(讀寫鎖)Java程式設計
- 使用 Java 讀寫 JMeter 中的變數JavaJMeter變數
- Java併發基礎-鎖的使用及原理(可重入鎖、讀寫鎖、內建鎖、訊號量等)Java
- 【java併發程式設計】ReentrantLock 可重入讀寫鎖Java程式設計ReentrantLock
- Eureka中讀寫鎖的奇思妙想,學廢了嗎?
- Lock介面、重入鎖ReentrantLock、讀寫鎖ReentrantReadWriteLockReentrantLock
- Go語言之讀寫鎖Go
- Java 中的鎖Java
- Java 讀寫鎖 ReadWriteLock 原理與應用場景詳解Java
- java併發程式設計-StampedLock高效能讀寫鎖Java程式設計
- 【JavaSE】Lock鎖和synchronized鎖的比較,lock鎖的特性,讀寫鎖的實現。Javasynchronized
- Go語言中的互斥鎖和讀寫鎖(Mutex和RWMutex)GoMutex
- 深入理解讀寫鎖ReentrantReadWriteLock
- JUC之讀寫鎖問題
- ReentrantReadWriteLock讀寫鎖及其在 RxCach
- Java 中的死鎖Java
- 扯扯Java中的鎖Java
- Java中的ReentrantLock鎖JavaReentrantLock
- Java併發包原始碼學習系列:ReentrantReadWriteLock讀寫鎖解析Java原始碼
- Java中的樂觀鎖——無鎖策略Java
- Java中的公平鎖和非公平鎖Java