ReentrantReadWriteLock读写锁详解
一、讀寫鎖簡介
現(xiàn)實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應(yīng)該允許多個線程同時讀取共享資源;但是如果一個線程想去寫這些共享資源,就不應(yīng)該允許其他線程對該資源進(jìn)行讀和寫的操作了。
針對這種場景,JAVA的并發(fā)包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操作相關(guān)的鎖,稱為共享鎖;一個是寫相關(guān)的鎖,稱為排他鎖,描述如下:
線程進(jìn)入讀鎖的前提條件:
沒有其他線程的寫鎖,
沒有寫請求或者有寫請求,但調(diào)用線程和持有鎖的線程是同一個
線程進(jìn)入寫鎖的前提條件:
沒有其他線程的讀鎖
沒有其他線程的寫鎖
而讀寫鎖有以下三個重要的特性:
二、源碼解讀
1、內(nèi)部類
讀寫鎖實現(xiàn)類中有許多內(nèi)部類,我們先來看下這些類的定義:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable
讀寫鎖并沒有實現(xiàn)Lock接口,而是實現(xiàn)了ReadWriteLock。并發(fā)系列中真正實現(xiàn)Lock接口的并不多,除了前面提到過的重入鎖(ReentrantLock),另外就是讀寫鎖中為了實現(xiàn)讀鎖和寫鎖的兩個內(nèi)部類:
public static class ReadLock implements Lock, java.io.Serializable
public static class WriteLock implements Lock, java.io.Serializable
另外讀寫鎖也設(shè)計成模板方法模式,通過繼承隊列同步器,提供了公平與非公平鎖的特性:
static abstract class Sync extends AbstractQueuedSynchronizer
final static class NonfairSync extends Sync
final static class FairSync extends Sync
2、讀寫狀態(tài)的設(shè)計
同步狀態(tài)在前面重入鎖的實現(xiàn)中是表示被同一個線程重復(fù)獲取的次數(shù),即一個整形變量來維護(hù),但是之前的那個表示僅僅表示是否鎖定,而不用區(qū)分是讀鎖還是寫鎖。而讀寫鎖需要在同步狀態(tài)(一個整形變量)上維護(hù)多個讀線程和一個寫線程的狀態(tài)。
讀寫鎖對于同步狀態(tài)的實現(xiàn)是在一個整形變量上通過“按位切割使用”:將變量切割成兩部分,高16位表示讀,低16位表示寫。
假設(shè)當(dāng)前同步狀態(tài)值為S,get和set的操作如下:
1、獲取寫狀態(tài):
S&0x0000FFFF:將高16位全部抹去
2、獲取讀狀態(tài):
S>>>16:無符號補0,右移16位
3、寫狀態(tài)加1:
S+1
4、讀狀態(tài)加1:
S+(1<<16)即S + 0x00010000
在代碼層嗎的判斷中,如果S不等于0,當(dāng)寫狀態(tài)(S&0x0000FFFF),而讀狀態(tài)(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是獲取當(dāng)前鎖狀態(tài);w是獲取寫鎖的狀態(tài)。
2、如果鎖狀態(tài)不為零,而寫鎖的狀態(tài)為0,則表示讀鎖狀態(tài)不為0,所以當(dāng)前線程不能獲取寫鎖。或者鎖狀態(tài)不為零,而寫鎖的狀態(tài)也不為0,但是獲取寫鎖的線程不是當(dāng)前線程,則當(dāng)前線程不能獲取寫鎖。
寫鎖是一個可重入的排它鎖,在獲取同步狀態(tài)時,增加了一個讀鎖是否存在的判斷。
寫鎖的釋放與ReentrantLock的釋放過程類似,每次釋放將寫狀態(tài)減1,直到寫狀態(tài)為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、讀鎖是一個支持重進(jìn)入的共享鎖,可以被多個線程同時獲取。
2、在沒有寫狀態(tài)為0時,讀鎖總會被成功獲取,而所做的也只是增加讀狀態(tài)(線程安全)
3、讀狀態(tài)是所有線程獲取讀鎖次數(shù)的總和,而每個線程各自獲取讀鎖的次數(shù)只能選擇保存在ThreadLocal中,由線程自身維護(hù)。
讀鎖的每次釋放均減小狀態(tài)(線程安全的,可能有多個讀線程同時釋放鎖),減小的值是1<<16。
5、鎖降級
鎖降級指的是寫鎖降級為讀鎖:把持住當(dāng)前擁有的寫鎖,再獲取到讀鎖,隨后釋放先前擁有的寫鎖的過程。
而鎖升級是將讀鎖變成寫鎖,但是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");
這種線獲取讀鎖,不釋放緊接著獲取寫鎖,會導(dǎo)致死鎖!!!
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");
這個過程跟上面的剛好相反,程序可以正常運行不會出現(xiàn)死鎖。但是鎖降級并不會自動釋放寫鎖。仍然需要顯示的釋放。
由于讀寫鎖用于讀多寫少的場景,天然的使用于實現(xiàn)緩存,下面看一個簡易的實現(xiàn)緩存的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 //這里需要再次判斷,防止后面阻塞的線程再次放入數(shù)據(jù)
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、業(yè)務(wù)邏輯很好理解,一個線程進(jìn)來先獲取讀鎖,如果map里面沒有值,則釋放讀鎖,獲取寫鎖,將該線程的value放入map中。
2、這里有兩次value為空的判斷,第一次判斷很好理解,第二次判斷是防止當(dāng)前線程在獲取寫鎖的時候,其他的線程阻塞在獲取寫鎖的地方。當(dāng)當(dāng)前線程將vaule放入map之后,釋放寫鎖。如果這個位置沒有value的判斷,后續(xù)獲得寫鎖的線程以為map仍然為空,會再一次將value值放入map中,覆蓋前面的value值,顯然這不是我們愿意看見的。
3、在第35行的位置,這里處理的鎖降級的邏輯。按照我們正常的邏輯思維,因為是先釋放寫鎖,再獲取讀鎖。那么鎖降級為什么要這么處理呢?答案是為了保證數(shù)據(jù)的可見性,因為如果當(dāng)前線程不獲取讀鎖而是直接釋放寫鎖,如果該線程在釋放寫鎖與獲取讀鎖這個時間段內(nèi),有另外一個線程獲取的寫鎖并修改了數(shù)據(jù),那么當(dāng)前線程無法感知數(shù)據(jù)的變更。如果按照鎖降級的原則來處理,那么當(dāng)前線程獲取到讀鎖之后,會阻塞其他線程獲取寫鎖,那么數(shù)據(jù)就不會被其他線程所改動,這樣就保證了數(shù)據(jù)的一致性。
總結(jié)
以上是生活随笔為你收集整理的ReentrantReadWriteLock读写锁详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 针对iPhone的pt、Android的
- 下一篇: React 如何实现骨架屏的展示-Sus