lock實作,讀寫鎖更複雜一些。假設你的程式中涉及到對一些共享資源的讀和寫操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,兩個線程同時讀一
個資源沒有任何問題,是以應該允許多個線程能在同時讀取共享資源。但是如果有一個線程想去寫這些共享資源,就不應該再有其它線程對該資源進行讀或寫(譯者注:也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。這就需要一個讀/寫鎖來解決這個問題。
java5在java.util.concurrent包中已經包含了讀寫鎖。盡管如此,我們還是應該了解其實作背後的原理。
以下是本文的主題
<a href="http://ifeve.com/read-write-locks/#simple">讀/寫鎖的java實作(read / write lock java implementation)</a>
<a href="http://ifeve.com/read-write-locks/#reentrance">讀/寫鎖的重入(read / write lock reentrance)</a>
<a href="http://ifeve.com/read-write-locks/#readreentrance">讀鎖重入(read reentrance)</a>
<a href="http://ifeve.com/read-write-locks/#writereentrance">寫鎖重入(write reentrance)</a>
<a href="http://ifeve.com/read-write-locks/#upgrade">讀鎖更新到寫鎖(read to write reentrance)</a>
<a href="http://ifeve.com/read-write-locks/#downgrade">寫鎖降級到讀鎖(write to read reentrance)</a>
<a href="http://ifeve.com/read-write-locks/#full">可重入的readwritelock的完整實作(fully reentrant readwritelock)</a>
<a href="http://ifeve.com/read-write-locks/#finally">在finally中調用unlock() (calling unlock() from a finally-clause)</a>
先讓我們對讀寫通路資源的條件做個概述:
<b>讀取</b> 沒有線程正在做寫操作,且沒有線程在請求寫操作。
<b>寫入</b> 沒有線程正在做讀寫操作。
如果某個線程想要讀取資源,隻要沒有線程正在對該資源進行寫操作且沒有線程請求對該資源的寫操作即可。我們假設對寫操作的請求比對讀操作的請求更重要,就
要提升寫請求的優先級。此外,如果讀操作發生的比較頻繁,我們又沒有提升寫操作的優先級,那麼就會産生“饑餓”現象。請求寫操作的線程會一直阻塞,直到所
有的讀線程都從readwritelock上解鎖了。如果一直保證新線程的讀操作權限,那麼等待寫操作的線程就會一直阻塞下去,結果就是發生“饑餓”。因
此,隻有當沒有線程正在鎖住readwritelock進行寫操作,且沒有線程請求該鎖準備執行寫操作時,才能保證讀操作繼續。
當其它線程沒有對共享資源進行讀操作或者寫操作時,某個線程就有可能獲得該共享資源的寫鎖,進而對共享資源進行寫操作。有多少線程請求了寫鎖以及以何種順序請求寫鎖并不重要,除非你想保證寫鎖請求的公平性。
按照上面的叙述,簡單的實作出一個讀/寫鎖,代碼如下
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修改後的的代碼:
代碼中我們可以看到,隻有在沒有線程擁有寫鎖的情況下才允許讀鎖的重入。此外,重入的讀鎖比寫鎖優先級高。
僅當一個線程已經持有寫鎖,才允許寫鎖重入(再次獲得寫鎖)。下面是方法lockwrite和unlockwrite修改後的的代碼。
注意在确定目前線程是否能夠擷取寫鎖的時候,是如何處理的。
有時,我們希望一個擁有讀鎖的線程,也能獲得寫鎖。想要允許這樣的操作,要求這個線程是唯一一個擁有讀鎖的線程。writelock()需要做點改動來達到這個目的:
現在readwritelock類就可以從讀鎖更新到寫鎖了。
有時擁有寫鎖的線程也希望得到讀鎖。如果一個線程擁有了寫鎖,那麼自然其它線程是不可能擁有讀鎖或寫鎖了。是以對于一個擁有寫鎖的線程,再獲得讀鎖,是不會有什麼危險的。我們僅僅需要對上面cangrantreadaccess方法進行簡單地修改:
下面是完整的readwritelock實作。為了便于代碼的閱讀與了解,簡單對上面的代碼做了重構。重構後的代碼如下。
在利用readwritelock來保護臨界區時,如果臨界區可能抛出異常,在finally塊中調用readunlock()和
writeunlock()就顯得很重要了。這樣做是為了保證readwritelock能被成功解鎖,然後其它線程可以請求到該鎖。這裡有個例子:
上面這樣的代碼結構能夠保證臨界區中抛出異常時readwritelock也會被釋放。如果unlockwrite方法不是在finally塊中調用的,
當臨界區抛出了異常時,readwritelock
會一直保持在寫鎖定狀态,就會導緻所有調用lockread()或lockwrite()的線程一直阻塞。唯一能夠重新解鎖readwritelock的
因素可能就是readwritelock是可重入的,當抛出異常時,這個線程後續還可以成功擷取這把鎖,然後執行臨界區以及再次調用
unlockwrite(),這就會再次釋放readwritelock。但是如果該線程後續不再擷取這把鎖了呢?是以,在finally中調用
unlockwrite對寫出健壯代碼是很重要的。