Java多執行緒(十)之ReentrantReadWriteLock深入分析

五柳-先生發表於2015-11-02

一、ReentrantReadWriteLock與ReentrantLock


  說到ReentrantReadWriteLock,首先要做的是與ReentrantLock劃清界限。它和後者都是單獨的實現,彼此之間沒有繼承或實現的關係。

ReentrantLock 實現了標準的互斥操作,也就是一次只能有一個執行緒持有鎖,也即所謂獨佔鎖的概念。前面的章節中一直在強調這個特點。顯然這個特點在一定程度上面減低了吞吐量,實際上獨佔鎖是一種保守的鎖策略,在這種情況下任何“讀/讀”,“寫/讀”,“寫/寫”操作都不能同時發生。但是同樣需要強調的一個概念是,鎖是有一定的開銷的,當併發比較大的時候,鎖的開銷就比較客觀了。所以如果可能的話就儘量少用鎖,非要用鎖的話就嘗試看能否改造為讀寫鎖。

ReadWriteLock 描述的是:一個資源能夠被多個讀執行緒訪問,或者被一個寫執行緒訪問,但是不能同時存在讀寫執行緒。也就是說讀寫鎖使用的場合是一個共享資源被大量讀取操作,而只有少量的寫操作(修改資料)。清單0描述了ReadWriteLock的API。

 

清單0 ReadWriteLock 介面

[java] view plaincopy
  1. public interface ReadWriteLock {  
  2.     Lock readLock();  
  3.     Lock writeLock();  
  4. }  

清單0描述的ReadWriteLock結構,這裡需要說明的是ReadWriteLock並不是Lock的子介面,只不過ReadWriteLock藉助Lock來實現讀寫兩個視角。在ReadWriteLock中每次讀取共享資料就需要讀取鎖,當需要修改共享資料時就需要寫入鎖。看起來好像是兩個鎖,但其實不盡然,下文會指出。


二、ReentrantReadWriteLock的特性


ReentrantReadWriteLock有以下幾個特性:

  • 公平性
    • 非公平鎖(預設) 這個和獨佔鎖的非公平性一樣,由於讀執行緒之間沒有鎖競爭,所以讀操作沒有公平性和非公平性,寫操作時,由於寫操作可能立即獲取到鎖,所以會推遲一個或多個讀操作或者寫操作。因此非公平鎖的吞吐量要高於公平鎖。
    • 公平鎖 利用AQS的CLH佇列,釋放當前保持的鎖(讀鎖或者寫鎖)時,優先為等待時間最長的那個寫執行緒分配寫入鎖,當前前提是寫執行緒的等待時間要比所有讀執行緒的等待時間要長。同樣一個執行緒持有寫入鎖或者有一個寫執行緒已經在等待了,那麼試圖獲取公平鎖的(非重入)所有執行緒(包括讀寫執行緒)都將被阻塞,直到最先的寫執行緒釋放鎖。如果讀執行緒的等待時間比寫執行緒的等待時間還有長,那麼一旦上一個寫執行緒釋放鎖,這一組讀執行緒將獲取鎖。
  • 重入性
    • 讀寫鎖允許讀執行緒和寫執行緒按照請求鎖的順序重新獲取讀取鎖或者寫入鎖。當然了只有寫執行緒釋放了鎖,讀執行緒才能獲取重入鎖。
    • 寫執行緒獲取寫入鎖後可以再次獲取讀取鎖,但是讀執行緒獲取讀取鎖後卻不能獲取寫入鎖。
    • 另外讀寫鎖最多支援65535個遞迴寫入鎖和65535個遞迴讀取鎖。
  • 鎖降級
    • 寫執行緒獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。
  • 鎖升級
    • 讀取鎖是不能直接升級為寫入鎖的。因為獲取一個寫入鎖需要釋放所有讀取鎖,所以如果有兩個讀取鎖檢視獲取寫入鎖而都不釋放讀取鎖時就會發生死鎖。
  • 鎖獲取中斷
    • 讀取鎖和寫入鎖都支援獲取鎖期間被中斷。這個和獨佔鎖一致。
  • 條件變數
    • 寫入鎖提供了條件變數(Condition)的支援,這個和獨佔鎖一致,但是讀取鎖卻不允許獲取條件變數,將得到一個UnsupportedOperationException異常。
  • 重入數
    • 讀取鎖和寫入鎖的數量最大分別只能是65535(包括重入數)。


三、ReentrantReadWriteLock的內部實現


3.1 讀寫鎖是獨佔鎖的兩個不同檢視


ReentrantReadWriteLock裡面的鎖主體就是一個Sync,也就是上面提到的FairSync或者NonfairSync,所以說實際上只有一個鎖,只是在獲取讀取鎖和寫入鎖的方式上不一樣,所以前面才有讀寫鎖是獨佔鎖的兩個不同檢視一說。

ReentrantReadWriteLock裡面有兩個類:ReadLock/WriteLock,這兩個類都是Lock的實現。

清單1 ReadLock 片段

[java] view plaincopy
  1. public static class ReadLock implements Lock, java.io.Serializable  {  
  2.     private final Sync sync;  
  3.   
  4.     protected ReadLock(ReentrantReadWriteLock lock) {  
  5.         sync = lock.sync;  
  6.     }  
  7.   
  8.     public void lock() {  
  9.         sync.acquireShared(1);  
  10.     }  
  11.   
  12.     public void lockInterruptibly() throws InterruptedException {  
  13.         sync.acquireSharedInterruptibly(1);  
  14.     }  
  15.   
  16.     public  boolean tryLock() {  
  17.         return sync.tryReadLock();  
  18.     }  
  19.   
  20.     public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {  
  21.         return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));  
  22.     }  
  23.   
  24.     public  void unlock() {  
  25.         sync.releaseShared(1);  
  26.     }  
  27.   
  28.     public Condition newCondition() {  
  29.         throw new UnsupportedOperationException();  
  30.     }  
  31.   
  32. }  


清單2 WriteLock 片段

[java] view plaincopy
  1. public static class WriteLock implements Lock, java.io.Serializable  {  
  2.     private final Sync sync;  
  3.     protected WriteLock(ReentrantReadWriteLock lock) {  
  4.         sync = lock.sync;  
  5.     }  
  6.     public void lock() {  
  7.         sync.acquire(1);  
  8.     }  
  9.   
  10.     public void lockInterruptibly() throws InterruptedException {  
  11.         sync.acquireInterruptibly(1);  
  12.     }  
  13.   
  14.     public boolean tryLock( ) {  
  15.         return sync.tryWriteLock();  
  16.     }  
  17.   
  18.     public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {  
  19.         return sync.tryAcquireNanos(1, unit.toNanos(timeout));  
  20.     }  
  21.   
  22.     public void unlock() {  
  23.         sync.release(1);  
  24.     }  
  25.   
  26.     public Condition newCondition() {  
  27.         return sync.newCondition();  
  28.     }  
  29.   
  30.     public boolean isHeldByCurrentThread() {  
  31.         return sync.isHeldExclusively();  
  32.     }  
  33.   
  34.     public int getHoldCount() {  
  35.         return sync.getWriteHoldCount();  
  36.     }  
  37. }  


清單1描述的是讀鎖的實現,清單2描述的是寫鎖的實現。顯然WriteLock就是一個獨佔鎖,這和ReentrantLock裡面的實現幾乎相同,都是使用了AQS的acquire/release操作。當然了在內部處理方式上與ReentrantLock還是有一點不同的。對比清單1和清單2可以看到,ReadLock獲取的是共享鎖,WriteLock獲取的是獨佔鎖。


3.2 ReentrantReadWriteLock中的state


在AQS章節中介紹到AQS中有一個state欄位(int型別,32位)用來描述有多少執行緒獲持有鎖。在獨佔鎖的時代這個值通常是0或者1(如果是重入的就是重入的次數),在共享鎖的時代就是持有鎖的數量。在上一節中談到,ReadWriteLock的讀、寫鎖是相關但是又不一致的,所以需要兩個數來描述讀鎖(共享鎖)和寫鎖(獨佔鎖)的數量。顯然現在一個state就不夠用了。於是在ReentrantReadWrilteLock裡面將這個欄位一分為二,高位16位表示共享鎖的數量,低位16位表示獨佔鎖的數量(或者重入數量)。2^16-1=65536,這就是上節中提到的為什麼共享鎖和獨佔鎖的數量最大隻能是65535的原因了。


3.3 讀寫鎖的獲取和釋放


有了上面的知識後再來分析讀寫鎖的獲取和釋放就容易多了。

清單3 寫入鎖獲取片段

[java] view plaincopy
  1. protected final boolean tryAcquire(int acquires) {  
  2.     Thread current = Thread.currentThread();  
  3.     int c = getState();  
  4.     int w = exclusiveCount(c);  
  5.     if (c != 0) {  
  6.         if (w == 0 || current != getExclusiveOwnerThread())  
  7.             return false;  
  8.         if (w + exclusiveCount(acquires) > MAX_COUNT)  
  9.             throw new Error("Maximum lock count exceeded");  
  10.     }  
  11.     if ((w == 0 && writerShouldBlock(current)) ||  
  12.         !compareAndSetState(c, c + acquires))  
  13.         return false;  
  14.     setExclusiveOwnerThread(current);  
  15.     return true;  
  16. }  


清單3 是寫入鎖獲取的邏輯片段,整個工作流程是這樣的:

    1. 持有鎖執行緒數非0(c=getState()不為0),如果寫執行緒數(w)為0(那麼讀執行緒數就不為0)或者獨佔鎖執行緒(持有鎖的執行緒)不是當前執行緒就返回失敗,或者寫入鎖的數量(其實是重入數)大於65535就丟擲一個Error異常。否則進行2。
    2. 如果當且寫執行緒數位0(那麼讀執行緒也應該為0,因為步驟1已經處理c!=0的情況),並且當前執行緒需要阻塞那麼就返回失敗;如果增加寫執行緒數失敗也返回失敗。否則進行3。
    3. 設定獨佔執行緒(寫執行緒)為當前執行緒,返回true。

清單3 中 exclusiveCount(c)就是獲取寫執行緒數(包括重入數),也就是state的低16位值。另外這裡有一段邏輯是當前寫執行緒是否需要阻塞writerShouldBlock(current)。清單4 和清單5 就是公平鎖和非公平鎖中是否需要阻塞的片段。很顯然對於非公平鎖而言總是不阻塞當前執行緒,而對於公平鎖而言如果AQS佇列不為空或者當前執行緒不是在AQS的佇列頭那麼就阻塞執行緒,直到佇列前面的執行緒處理完鎖邏輯。

清單4 公平讀寫鎖寫執行緒是否阻塞

[java] view plaincopy
  1. final boolean writerShouldBlock(Thread current) {  
  2.     return !isFirst(current);  
  3. }  


清單5 非公平讀寫鎖寫執行緒是否阻塞

[java] view plaincopy
  1. final boolean writerShouldBlock(Thread current) {  
  2.     return false;  
  3. }  


寫入鎖的獲取邏輯清楚後,釋放鎖就比較簡單了。清單6 描述的寫入鎖釋放邏輯片段,其實就是檢測下剩下的寫入鎖數量,如果是0就將獨佔鎖執行緒清空(意味著沒有執行緒獲取鎖),否則就是說當前是重入鎖的一次釋放,所以不能將獨佔鎖執行緒清空。然後將剩餘執行緒狀態數寫回AQS。

清單6 寫入鎖釋放邏輯片段

[java] view plaincopy
  1. protected final boolean tryRelease(int releases) {  
  2.     int nextc = getState() - releases;  
  3.     if (Thread.currentThread() != getExclusiveOwnerThread())  
  4.         throw new IllegalMonitorStateException();  
  5.     if (exclusiveCount(nextc) == 0) {  
  6.         setExclusiveOwnerThread(null);  
  7.         setState(nextc);  
  8.         return true;  
  9.     } else {  
  10.         setState(nextc);  
  11.         return false;  
  12.     }  
  13. }  


清單3~6 描述的寫入鎖的獲取釋放過程。讀取鎖的獲取和釋放過程要稍微複雜些。 清單7描述的是讀取鎖的獲取過程。

清單7 讀取鎖獲取過程片段

[java] view plaincopy
  1. protected final int tryAcquireShared(int unused) {  
  2.     Thread current = Thread.currentThread();  
  3.     int c = getState();  
  4.     if (exclusiveCount(c) != 0 &&  
  5.         getExclusiveOwnerThread() != current)  
  6.         return -1;  
  7.     if (sharedCount(c) == MAX_COUNT)  
  8.         throw new Error("Maximum lock count exceeded");  
  9.     if (!readerShouldBlock(current) &&  
  10.         compareAndSetState(c, c + SHARED_UNIT)) {  
  11.         HoldCounter rh = cachedHoldCounter;  
  12.         if (rh == null || rh.tid != current.getId())  
  13.             cachedHoldCounter = rh = readHolds.get();  
  14.         rh.count++;  
  15.         return 1;  
  16.     }  
  17.     return fullTryAcquireShared(current);  
  18. }  
  19.   
  20. final int fullTryAcquireShared(Thread current) {  
  21.     HoldCounter rh = cachedHoldCounter;  
  22.     if (rh == null || rh.tid != current.getId())  
  23.         rh = readHolds.get();  
  24.     for (;;) {  
  25.         int c = getState();  
  26.         int w = exclusiveCount(c);  
  27.         if ((w != 0 && getExclusiveOwnerThread() != current) ||  
  28.             ((rh.count | w) == 0 && readerShouldBlock(current)))  
  29.             return -1;  
  30.         if (sharedCount(c) == MAX_COUNT)  
  31.             throw new Error("Maximum lock count exceeded");  
  32.         if (compareAndSetState(c, c + SHARED_UNIT)) {  
  33.             cachedHoldCounter = rh; // cache for release  
  34.             rh.count++;  
  35.             return 1;  
  36.         }  
  37.     }  
  38. }  


讀取鎖獲取的過程是這樣的:

    1. 如果寫執行緒持有鎖(也就是獨佔鎖數量不為0),並且獨佔執行緒不是當前執行緒,那麼就返回失敗。因為允許寫入執行緒獲取鎖的同時獲取讀取鎖。否則進行2。
    2. 如果讀執行緒請求鎖數量達到了65535(包括重入鎖),那麼就跑出一個錯誤Error,否則進行3。
    3. 如果讀執行緒不用等待(實際上是是否需要公平鎖),並且增加讀取鎖狀態數成功,那麼就返回成功,否則進行4。
    4. 步驟3失敗的原因是CAS操作修改狀態數失敗,那麼就需要迴圈不斷嘗試去修改狀態直到成功或者鎖被寫入執行緒佔有。實際上是過程3的不斷嘗試直到CAS計數成功或者被寫入執行緒佔有鎖。


3.4 HoldCounter


在清單7 中有一個物件HoldCounter,這裡暫且不提這是什麼結構和為什麼存在這樣一個結構。

接下來根據清單8 我們來看如何釋放一個讀取鎖。同樣先不理HoldCounter,關鍵的在於for迴圈裡面,其實就是一個不斷嘗試的CAS操作,直到修改狀態成功。前面說過state的高16位描述的共享鎖(讀取鎖)的數量,所以每次都需要減去2^16,這樣就相當於讀取鎖數量減1。實際上SHARED_UNIT=1<<16。

清單8 讀取鎖釋放過程

[java] view plaincopy
  1. protected final boolean tryReleaseShared(int unused) {  
  2.     HoldCounter rh = cachedHoldCounter;  
  3.     Thread current = Thread.currentThread();  
  4.     if (rh == null || rh.tid != current.getId())  
  5.         rh = readHolds.get();  
  6.     if (rh.tryDecrement() <= 0)  
  7.         throw new IllegalMonitorStateException();  
  8.     for (;;) {  
  9.         int c = getState();  
  10.         int nextc = c - SHARED_UNIT;  
  11.         if (compareAndSetState(c, nextc))  
  12.             return nextc == 0;  
  13.     }  
  14. }  


好了,現在回頭看HoldCounter到底是一個什麼東西。首先我們可以看到只有在獲取共享鎖(讀取鎖)的時候加1,也只有在釋放共享鎖的時候減1有作用,並且在釋放鎖的時候丟擲了一個IllegalMonitorStateException異常。而我們知道IllegalMonitorStateException通常描述的是一個執行緒操作一個不屬於自己的監視器物件的引發的異常。也就是說這裡的意思是一個執行緒釋放了一個不屬於自己或者不存在的共享鎖。

前面的章節中一再強調,對於共享鎖,其實並不是鎖的概念,更像是計數器的概念。一個共享鎖就相對於一次計數器操作,一次獲取共享鎖相當於計數器加1,釋放一個共享鎖就相當於計數器減1。顯然只有執行緒持有了共享鎖(也就是當前執行緒攜帶一個計數器,描述自己持有多少個共享鎖或者多重共享鎖),才能釋放一個共享鎖。否則一個沒有獲取共享鎖的執行緒呼叫一次釋放操作就會導致讀寫鎖的state(持有鎖的執行緒數,包括重入數)錯誤。

明白了HoldCounter的作用後我們就可以猜到它的作用其實就是當前執行緒持有共享鎖(讀取鎖)的數量,包括重入的數量。那麼這個數量就必須和執行緒繫結在一起。

在Java裡面將一個物件和執行緒繫結在一起,就只有ThreadLocal才能實現了。所以毫無疑問HoldCounter就應該是繫結到執行緒上的一個計數器。

清單9 執行緒持有讀取鎖數量的計數器

[java] view plaincopy
  1. static final class HoldCounter {  
  2.     int count;  
  3.     final long tid = Thread.currentThread().getId();  
  4.     int tryDecrement() {  
  5.         int c = count;  
  6.         if (c > 0)  
  7.             count = c - 1;  
  8.         return c;  
  9.     }  
  10. }  
  11.   
  12. static final class ThreadLocalHoldCounter  
  13.     extends ThreadLocal<HoldCounter> {  
  14.     public HoldCounter initialValue() {  
  15.         return new HoldCounter();  
  16.     }  
  17. }  


清單9 描述的是執行緒持有讀取鎖數量的計數器。可以看到這裡使用ThreadLocal將HoldCounter繫結到當前執行緒上,同時HoldCounter也持有執行緒Id,這樣在釋放鎖的時候才能知道ReadWriteLock裡面快取的上一個讀取執行緒(cachedHoldCounter)是否是當前執行緒。這樣做的好處是可以減少ThreadLocal.get()的次數,因為這也是一個耗時操作。需要說明的是這樣HoldCounter繫結執行緒id而不繫結執行緒物件的原因是避免HoldCounter和ThreadLocal互相繫結而GC難以釋放它們(儘管GC能夠智慧的發現這種引用而回收它們,但是這需要一定的代價),所以其實這樣做只是為了幫助GC快速回收物件而已。

除了readLock()和writeLock()外,Lock物件還允許tryLock(),那麼ReadLock和WriteLock的tryLock()不一樣。清單10 和清單11 分別描述了讀取鎖的tryLock()和寫入鎖的tryLock()。

讀取鎖tryLock()也就是tryReadLock()成功的條件是:沒有寫入鎖或者寫入鎖是當前執行緒,並且讀執行緒共享鎖數量沒有超過65535個。

寫入鎖tryLock()也就是tryWriteLock()成功的條件是: 沒有寫入鎖或者寫入鎖是當前執行緒,並且嘗試一次修改state成功。

清單10 讀取鎖的tryLock()

[java] view plaincopy
  1. final boolean tryReadLock() {  
  2.     Thread current = Thread.currentThread();  
  3.     for (;;) {  
  4.         int c = getState();  
  5.         if (exclusiveCount(c) != 0 &&  
  6.             getExclusiveOwnerThread() != current)  
  7.             return false;  
  8.         if (sharedCount(c) == MAX_COUNT)  
  9.             throw new Error("Maximum lock count exceeded");  
  10.         if (compareAndSetState(c, c + SHARED_UNIT)) {  
  11.             HoldCounter rh = cachedHoldCounter;  
  12.             if (rh == null || rh.tid != current.getId())  
  13.                 cachedHoldCounter = rh = readHolds.get();  
  14.             rh.count++;  
  15.             return true;  
  16.         }  
  17.     }  
  18. }  


清單11 寫入鎖的tryLock()

[java] view plaincopy
  1. final boolean tryWriteLock() {  
  2.     Thread current = Thread.currentThread();  
  3.     int c = getState();  
  4.     if (c != 0) {  
  5.         int w = exclusiveCount(c);  
  6.         if (w == 0 ||current != getExclusiveOwnerThread())  
  7.             return false;  
  8.         if (w == MAX_COUNT)  
  9.             throw new Error("Maximum lock count exceeded");  
  10.     }  
  11.     if (!compareAndSetState(c, c + 1))  
  12.         return false;  
  13.     setExclusiveOwnerThread(current);  
  14.     return true;  
  15. }  


四、小結


使用ReentrantReadWriteLock可以推廣到大部分讀,少量寫的場景,因為讀執行緒之間沒有競爭,所以比起sychronzied,效能好很多。
如果需要較為精確的控制快取,使用ReentrantReadWriteLock倒也不失為一個方案。



參考內容來源:

ReentrantReadWriteLock  http://uule.iteye.com/blog/1549707

深入淺出 Java Concurrency (13): 鎖機制 part 8 讀寫鎖 (ReentrantReadWriteLock) (1)

http://www.blogjava.net/xylz/archive/2010/07/14/326080.html
深入淺出 Java Concurrency (14): 鎖機制 part 9 讀寫鎖 (ReentrantReadWriteLock) (2)
http://www.blogjava.net/xylz/archive/2010/07/15/326152.html
高效能鎖ReentrantReadWriteLock
http://jhaij.iteye.com/blog/269656
JDK說明
http://www.cjsdn.net/Doc/JDK60/java/util/concurrent/locks/ReentrantReadWriteLock.html
關於concurrent包 執行緒池、資源封鎖和佇列、ReentrantReadWriteLock介紹

http://www.oschina.net/question/16_636

轉載: http://blog.csdn.net/vernonzheng/article/details/8297230

相關文章