ReentrantReadWriteLock读写锁详解
一、讀寫鎖簡介
現實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應該允許多個線程同時讀取共享資源;但是如果一個線程想去寫這些共享資源,就不應該允許其他線程對該資源進行讀和寫的操作了。
針對這種場景,JAVA的并發包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操作相關的鎖,稱為共享鎖;一個是寫相關的鎖,稱為排他鎖,描述如下:
線程進入讀鎖的前提條件:
沒有其他線程的寫鎖,
沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個
線程進入寫鎖的前提條件:
沒有其他線程的讀鎖
沒有其他線程的寫鎖
而讀寫鎖有以下三個重要的特性:
二、源碼解讀
1、內部類
讀寫鎖實現類中有許多內部類,我們先來看下這些類的定義:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable
讀寫鎖并沒有實現Lock接口,而是實現了ReadWriteLock。并發系列中真正實現Lock接口的并不多,除了前面提到過的重入鎖(ReentrantLock),另外就是讀寫鎖中為了實現讀鎖和寫鎖的兩個內部類:
public static class ReadLock implements Lock, java.io.Serializable
public static class WriteLock implements Lock, java.io.Serializable
另外讀寫鎖也設計成模板方法模式,通過繼承隊列同步器,提供了公平與非公平鎖的特性:
static abstract class Sync extends AbstractQueuedSynchronizer
final static class NonfairSync extends Sync
final static class FairSync extends Sync
2、讀寫狀態的設計
同步狀態在前面重入鎖的實現中是表示被同一個線程重復獲取的次數,即一個整形變量來維護,但是之前的那個表示僅僅表示是否鎖定,而不用區分是讀鎖還是寫鎖。而讀寫鎖需要在同步狀態(一個整形變量)上維護多個讀線程和一個寫線程的狀態。
讀寫鎖對于同步狀態的實現是在一個整形變量上通過“按位切割使用”:將變量切割成兩部分,高16位表示讀,低16位表示寫。
假設當前同步狀態值為S,get和set的操作如下:
1、獲取寫狀態:
S&0x0000FFFF:將高16位全部抹去
2、獲取讀狀態:
S>>>16:無符號補0,右移16位
3、寫狀態加1:
S+1
4、讀狀態加1:
S+(1<<16)即S + 0x00010000
在代碼層嗎的判斷中,如果S不等于0,當寫狀態(S&0x0000FFFF),而讀狀態(S>>>16)大于0,則表示該讀寫鎖的讀鎖已被獲取。
3、寫鎖的獲取與釋放
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if ((w == 0 && writerShouldBlock(current)) ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
1、c是獲取當前鎖狀態;w是獲取寫鎖的狀態。
2、如果鎖狀態不為零,而寫鎖的狀態為0,則表示讀鎖狀態不為0,所以當前線程不能獲取寫鎖。或者鎖狀態不為零,而寫鎖的狀態也不為0,但是獲取寫鎖的線程不是當前線程,則當前線程不能獲取寫鎖。
寫鎖是一個可重入的排它鎖,在獲取同步狀態時,增加了一個讀鎖是否存在的判斷。
寫鎖的釋放與ReentrantLock的釋放過程類似,每次釋放將寫狀態減1,直到寫狀態為0時,才表示該寫鎖被釋放了。
4、讀鎖的獲取與釋放
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 }
1、讀鎖是一個支持重進入的共享鎖,可以被多個線程同時獲取。
2、在沒有寫狀態為0時,讀鎖總會被成功獲取,而所做的也只是增加讀狀態(線程安全)
3、讀狀態是所有線程獲取讀鎖次數的總和,而每個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護。
讀鎖的每次釋放均減小狀態(線程安全的,可能有多個讀線程同時釋放鎖),減小的值是1<<16。
5、鎖降級
鎖降級指的是寫鎖降級為讀鎖:把持住當前擁有的寫鎖,再獲取到讀鎖,隨后釋放先前擁有的寫鎖的過程。
而鎖升級是將讀鎖變成寫鎖,但是ReentrantReadWriteLock不支持這種方式。
我們先來看鎖升級的程序:
1 ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
2 rwl.readLock().lock();
3 System.out.println("get readLock");
4 rwl.writeLock().lock();
5 System.out.println("get writeLock");
這種線獲取讀鎖,不釋放緊接著獲取寫鎖,會導致死鎖!!!
1 ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
2 rwl.writeLock().lock();
3 System.out.println("get writeLock");
4 rwl.readLock().lock();
5 System.out.println("get readLock");
這個過程跟上面的剛好相反,程序可以正常運行不會出現死鎖。但是鎖降級并不會自動釋放寫鎖。仍然需要顯示的釋放。
由于讀寫鎖用于讀多寫少的場景,天然的使用于實現緩存,下面看一個簡易的實現緩存的DEMO:
1 import java.util.HashMap;
2 import java.util.concurrent.locks.ReadWriteLock;
3 import java.util.concurrent.locks.ReentrantReadWriteLock;
4
5
6 public class CachedTest
7 {
8 volatile HashMap<String,String> cacheMap = new HashMap<String,String>();
9
10 ReadWriteLock rwLock = new ReentrantReadWriteLock();
11
12 public String getS(String key)
13 {
14 rwLock.readLock().lock();
15 String value = null;
16 try
17 {
18 if(cacheMap.get(key) == null)
19 {
20 rwLock.readLock().unlock();
21 rwLock.writeLock().lock();
22 try
23 {
24 //這里需要再次判斷,防止后面阻塞的線程再次放入數據
25 if(cacheMap.get(key) == null)
26 {
27 value = "" + Thread.currentThread().getName();
28 cacheMap.put(key, value);
29 System.out.println(Thread.currentThread().getName() + "put the value" + value);
30 }
31 }
32 finally
33 {
34 //這里是鎖降級,讀鎖的獲取與寫鎖的釋放順序不能顛倒
35 rwLock.readLock().lock();
36 rwLock.writeLock().unlock();
37 }
38 }
39 }
40 finally
41 {
42 rwLock.readLock().unlock();
43 }
44 return cacheMap.get(key);
45 }
46 }
1、業務邏輯很好理解,一個線程進來先獲取讀鎖,如果map里面沒有值,則釋放讀鎖,獲取寫鎖,將該線程的value放入map中。
2、這里有兩次value為空的判斷,第一次判斷很好理解,第二次判斷是防止當前線程在獲取寫鎖的時候,其他的線程阻塞在獲取寫鎖的地方。當當前線程將vaule放入map之后,釋放寫鎖。如果這個位置沒有value的判斷,后續獲得寫鎖的線程以為map仍然為空,會再一次將value值放入map中,覆蓋前面的value值,顯然這不是我們愿意看見的。
3、在第35行的位置,這里處理的鎖降級的邏輯。按照我們正常的邏輯思維,因為是先釋放寫鎖,再獲取讀鎖。那么鎖降級為什么要這么處理呢?答案是為了保證數據的可見性,因為如果當前線程不獲取讀鎖而是直接釋放寫鎖,如果該線程在釋放寫鎖與獲取讀鎖這個時間段內,有另外一個線程獲取的寫鎖并修改了數據,那么當前線程無法感知數據的變更。如果按照鎖降級的原則來處理,那么當前線程獲取到讀鎖之后,會阻塞其他線程獲取寫鎖,那么數據就不會被其他線程所改動,這樣就保證了數據的一致性。
總結
以上是生活随笔為你收集整理的ReentrantReadWriteLock读写锁详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 针对iPhone的pt、Android的
- 下一篇: mysql c api 封装_封装MyS