Java中的讀/寫鎖

weixin_34290000發表於2018-04-12

原文連結 作者:Jakob Jenkov 譯者:微涼 校對:丁一

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

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

以下是本文的主題

讀/寫鎖的Java實現(Read / Write Lock Java Implementation)

讀/寫鎖的重入(Read / Write Lock Reentrance)

讀鎖重入(Read Reentrance)

寫鎖重入(Write 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中的讀/寫鎖

相關文章