搞定ReentrantReadWriteLock 几道小小数学题就够了
ReadWriteLock
ReadWriteLock 直譯過來為【讀寫鎖】。現(xiàn)實(shí)中,讀多寫少的業(yè)務(wù)場(chǎng)景是非常普遍的,比如應(yīng)用緩存
一個(gè)線程將數(shù)據(jù)寫入緩存,其他線程可以直接讀取緩存中的數(shù)據(jù),提高數(shù)據(jù)查詢效率
之前提到的互斥鎖都是排他鎖,也就是說同一時(shí)刻只允許一個(gè)線程進(jìn)行訪問,當(dāng)面對(duì)可共享讀的業(yè)務(wù)場(chǎng)景,互斥鎖顯然是比較低效的一種處理方式。為了提高效率,讀寫鎖模型就誕生了
效率提升是一方面,但并發(fā)編程更重要的是在保證準(zhǔn)確性的前提下提高效率
一個(gè)寫線程改變了緩存中的值,其他讀線程一定是可以 “感知” 到的,否則可能導(dǎo)致查詢到的值不準(zhǔn)確
所以關(guān)于讀寫鎖模型就了下面這 3 條規(guī)定:
允許多個(gè)線程同時(shí)讀共享變量
只允許一個(gè)線程寫共享變量
如果寫線程正在執(zhí)行寫操作,此時(shí)則禁止其他讀線程讀共享變量
ReadWriteLock 是一個(gè)接口,其內(nèi)部只有兩個(gè)方法:
public?interface?ReadWriteLock?{//?返回用于讀的鎖Lock?readLock();//?返回用于寫的鎖Lock?writeLock(); }所以要了解整個(gè)讀/寫鎖的整個(gè)應(yīng)用過程,需要從它的實(shí)現(xiàn)類 ReentrantReadWriteLock 說起
ReentrantReadWriteLock 類結(jié)構(gòu)
直接對(duì)比ReentrantReadWriteLock 與 ReentrantLock的類結(jié)構(gòu)
他們又很相似吧,根據(jù)類名稱以及類結(jié)構(gòu),按照咱們前序文章的分析,你也就能看出 ReentrantReadWriteLock 的基本特性:
其中黃顏色標(biāo)記的的 鎖降級(jí) 是看不出來的, 這里先有個(gè)印象,下面會(huì)單獨(dú)說明
另外,不知道你是否還記得,Java AQS隊(duì)列同步器以及ReentrantLock的應(yīng)用 說過,Lock 和 AQS 同步器是一種組合形式的存在,既然這里是讀/寫兩種鎖,他們的組合模式也就分成了兩種:
讀鎖與自定義同步器的聚合
寫鎖與自定義同步器的聚合
這里只是提醒大家,模式?jīng)]有變,不要被讀/寫兩種鎖迷惑
基本示例
說了這么多,如果你忘了前序知識(shí),整體理解感覺應(yīng)該是有斷檔的,所以先來看個(gè)示例(模擬使用緩存)讓大家對(duì) ReentrantReadWriteLock 有個(gè)直觀的使用印象
public?class?ReentrantReadWriteLockCache?{//?定義一個(gè)非線程安全的?HashMap?用于緩存對(duì)象static?Map<String,?Object>?map?=?new?HashMap<String,?Object>();//?創(chuàng)建讀寫鎖對(duì)象static?ReadWriteLock?readWriteLock?=?new?ReentrantReadWriteLock();//?構(gòu)建讀鎖static?Lock?rl?=?readWriteLock.readLock();//?構(gòu)建寫鎖static?Lock?wl?=?readWriteLock.writeLock();public?static?final?Object?get(String?key)?{rl.lock();try{return?map.get(key);}finally?{rl.unlock();}}public?static?final?Object?put(String?key,?Object?value){wl.lock();try{return?map.put(key,?value);}finally?{wl.unlock();}} }你瞧,使用就是這么簡(jiǎn)單。但是你知道的,AQS 的核心是鎖的實(shí)現(xiàn),即控制同步狀態(tài) state 的值,ReentrantReadWriteLock 也是應(yīng)用AQS的 state 來控制同步狀態(tài)的,那么問題來了:
一個(gè) int 類型的 state 怎么既控制讀的同步狀態(tài),又可以控制寫的同步狀態(tài)呢?
顯然需要一點(diǎn)設(shè)計(jì)了
讀寫狀態(tài)設(shè)計(jì)
如果要在一個(gè) int 類型變量上維護(hù)多個(gè)狀態(tài),那肯定就需要拆分了。我們知道 int 類型數(shù)據(jù)占32位,所以我們就有機(jī)會(huì)按位切割使用state了。我們將其切割成兩部分:
高16位表示讀
低16位表示寫
所以,要想準(zhǔn)確的計(jì)算讀/寫各自的狀態(tài)值,肯定就要應(yīng)用位運(yùn)算了,下面代碼是 JDK1.8,ReentrantReadWriteLock 自定義同步器 Sync 的位操作
abstract?static?class?Sync?extends?AbstractQueuedSynchronizer?{static?final?int?SHARED_SHIFT???=?16;static?final?int?SHARED_UNIT????=?(1?<<?SHARED_SHIFT);static?final?int?MAX_COUNT??????=?(1?<<?SHARED_SHIFT)?-?1;static?final?int?EXCLUSIVE_MASK?=?(1?<<?SHARED_SHIFT)?-?1;static?int?sharedCount(int?c)?{?return?c?>>>?SHARED_SHIFT;?}static?int?exclusiveCount(int?c)?{?return?c?&?EXCLUSIVE_MASK;?} }乍一看真是有些復(fù)雜的可怕,別慌,咱們通過幾道小小數(shù)學(xué)題就可以搞定整個(gè)位運(yùn)算過程
整個(gè) ReentrantReadWriteLock 中 讀/寫狀態(tài)的計(jì)算就是反復(fù)應(yīng)用這幾道數(shù)學(xué)題,所以,在閱讀下面內(nèi)容之前,希望你搞懂這簡(jiǎn)單的運(yùn)算
基礎(chǔ)鋪墊足夠了,我們進(jìn)入源碼分析吧
源碼分析
寫鎖分析
由于寫鎖是排他的,所以肯定是要重寫 AQS 中 tryAcquire 方法
????????protected?final?boolean?tryAcquire(int?acquires)?{????????Thread?current?=?Thread.currentThread();//?獲取?state?整體的值int?c?=?getState();//?獲取寫狀態(tài)的值int?w?=?exclusiveCount(c);if?(c?!=?0)?{//?w=0:?根據(jù)推理二,整體狀態(tài)不等于零,寫狀態(tài)等于零,所以,讀狀態(tài)大于0,即存在讀鎖//?或者當(dāng)前線程不是已獲取寫鎖的線程//?二者之一條件成真,則獲取寫狀態(tài)失敗if?(w?==?0?||?current?!=?getExclusiveOwnerThread())return?false;if?(w?+?exclusiveCount(acquires)?>?MAX_COUNT)throw?new?Error("Maximum?lock?count?exceeded");//?根據(jù)推理一第?1?條,更新寫狀態(tài)值setState(c?+?acquires);return?true;}if?(writerShouldBlock()?||!compareAndSetState(c,?c?+?acquires))return?false;setExclusiveOwnerThread(current);return?true;}上述代碼 第 19 行 writerShouldBlock 也并沒有什么神秘的,只不過是公平/非公平獲取鎖方式的判斷(是否有前驅(qū)節(jié)點(diǎn)來判斷)
你瞧,寫鎖獲取方式就是這么簡(jiǎn)單
讀鎖分析
由于讀鎖是共享式的,所以肯定是要重寫 AQS 中 tryAcquireShared 方法
????????protected?final?int?tryAcquireShared(int?unused)?{Thread?current?=?Thread.currentThread();int?c?=?getState();//?寫狀態(tài)不等于0,并且鎖的持有者不是當(dāng)前線程,根據(jù)約定?3,則獲取讀鎖失敗if?(exclusiveCount(c)?!=?0?&&getExclusiveOwnerThread()?!=?current)return?-1;//?獲取讀狀態(tài)值int?r?=?sharedCount(c);//?這個(gè)地方有點(diǎn)不一樣,我們單獨(dú)說明if?(!readerShouldBlock()?&&r?<?MAX_COUNT?&&compareAndSetState(c,?c?+?SHARED_UNIT))?{if?(r?==?0)?{firstReader?=?current;firstReaderHoldCount?=?1;}?else?if?(firstReader?==?current)?{firstReaderHoldCount++;}?else?{HoldCounter?rh?=?cachedHoldCounter;if?(rh?==?null?||?rh.tid?!=?getThreadId(current))cachedHoldCounter?=?rh?=?readHolds.get();else?if?(rh.count?==?0)readHolds.set(rh);rh.count++;}return?1;}//?如果獲取讀鎖失敗則進(jìn)入自旋獲取return?fullTryAcquireShared(current);}readerShouldBlock 和 writerShouldBlock 在公平鎖的實(shí)現(xiàn)上都是判斷是否有前驅(qū)節(jié)點(diǎn),但是在非公平鎖的實(shí)現(xiàn)上,前者是這樣的:
final?boolean?readerShouldBlock()?{return?apparentlyFirstQueuedIsExclusive(); }final?boolean?apparentlyFirstQueuedIsExclusive()?{Node?h,?s;return?(h?=?head)?!=?null?&&//?等待隊(duì)列頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)(s?=?h.next)??!=?null?&&//?如果是排他式的節(jié)點(diǎn)!s.isShared()?????????&&s.thread?!=?null; }簡(jiǎn)單來說,如果請(qǐng)求讀鎖的當(dāng)前線程發(fā)現(xiàn)同步隊(duì)列的 head 節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)為排他式節(jié)點(diǎn),那么就說明有一個(gè)線程在等待獲取寫鎖(爭(zhēng)搶寫鎖失敗,被放入到同步隊(duì)列中),那么請(qǐng)求讀鎖的線程就要阻塞,畢竟讀多寫少,如果還沒有這點(diǎn)判斷機(jī)制,寫鎖可能會(huì)發(fā)生【饑餓】
上述條件都滿足了,也就會(huì)進(jìn)入 ?tryAcquireShared 代碼的第 14 行到第 25 行,這段代碼主要是為了記錄線程持有鎖的次數(shù)。讀鎖是共享式的,還想記錄每個(gè)線程持有讀鎖的次數(shù),就要用到 ThreadLocal 了,因?yàn)檫@不影響同步狀態(tài) state 的值,所以就不分析了, 只把關(guān)系放在這吧
到這里讀鎖的獲取也就結(jié)束了,比寫鎖稍稍復(fù)雜那么一丟丟,接下來就說明一下那個(gè)可能讓你迷惑的鎖升級(jí)/降級(jí)問題吧
??
讀寫鎖的升級(jí)與降級(jí)
個(gè)人理解:讀鎖是可以被多線程共享的,寫鎖是單線程獨(dú)占的,也就是說寫鎖的并發(fā)限制比讀鎖高,所以
在真正了解讀寫鎖的升級(jí)與降級(jí)之前,我們需要完善一下本文開頭 ReentrantReadWriteLock 的例子
?public?static?final?Object?get(String?key)?{Object?obj?=?null;rl.lock();try{//?獲取緩存中的值obj?=?map.get(key);}finally?{rl.unlock();}//?緩存中值不為空,直接返回if?(obj!=?null)?{return?obj;}//?緩存中值為空,則通過寫鎖查詢DB,并將其寫入到緩存中wl.lock();try{//?再次嘗試獲取緩存中的值obj?=?map.get(key);//?再次獲取緩存中值還是為空if?(obj?==?null)?{//?查詢DBobj?=?getDataFromDB(key);?//?偽代碼:getDataFromDB//?將其放入到緩存中map.put(key,?obj);}}finally?{wl.unlock();}return?obj;}有童鞋可能會(huì)有疑問
在寫鎖里面,為什么代碼第19行還要再次獲取緩存中的值呢?不是多此一舉嗎?
其實(shí)這里再次嘗試獲取緩存中的值是很有必要的,因?yàn)榭赡艽嬖诙鄠€(gè)線程同時(shí)執(zhí)行 get 方法,并且參數(shù) key 也是相同的,執(zhí)行到代碼第 16 行 wl.lock() ,比如這樣:
線程 A,B,C 同時(shí)執(zhí)行到臨界區(qū) wl.lock(), 只有線程 A 獲取寫鎖成功,線程B,C只能阻塞,直到線程A 釋放寫鎖。這時(shí),當(dāng)線程B 或者 C 再次進(jìn)入臨界區(qū)時(shí),線程 A 已經(jīng)將值更新到緩存中了,所以線程B,C沒必要再查詢一次DB,而是再次嘗試查詢緩存中的值
既然再次獲取緩存很有必要,我能否在讀鎖里直接判斷,如果緩存中沒有值,那就再次獲取寫鎖來查詢DB不就可以了嘛,就像這樣:
?public?static?final?Object?getLockUpgrade(String?key)?{Object?obj?=?null;rl.lock();try{obj?=?map.get(key);if?(obj?==?null){wl.lock();try{obj?=?map.get(key);if?(obj?==?null)?{obj?=?getDataFromDB(key);?//?偽代碼:getDataFromDBmap.put(key,?obj);}}finally?{wl.unlock();}}}finally?{rl.unlock();}return?obj;}這還真是不可以的,因?yàn)楂@取一個(gè)寫入鎖需要先釋放所有的讀取鎖,如果有兩個(gè)讀取鎖試圖獲取寫入鎖,且都不釋放讀取鎖時(shí),就會(huì)發(fā)生死鎖,所以在這里,鎖的升級(jí)是不被允許的
讀寫鎖的升級(jí)是不可以的,那么鎖的降級(jí)是可以的嘛?這個(gè)是 Oracle 官網(wǎng)關(guān)于鎖降級(jí)的示例?https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html
,我將代碼粘貼在此處,大家有興趣可以點(diǎn)進(jìn)去連接看更多內(nèi)容
代碼中聲明了一個(gè) volatile 類型的 cacheValid 變量,保證其可見性。
首先獲取讀鎖,如果cache不可用,則釋放讀鎖
然后獲取寫鎖
在更改數(shù)據(jù)之前,再檢查一次cacheValid的值,然后修改數(shù)據(jù),將cacheValid置為true
然后在釋放寫鎖前獲取讀鎖 此時(shí)
cache中數(shù)據(jù)可用,處理cache中數(shù)據(jù),最后釋放讀鎖
這個(gè)過程就是一個(gè)完整的鎖降級(jí)的過程,目的是保證數(shù)據(jù)可見性,聽起來很有道理的樣子,那么問題來了:
上述代碼為什么在釋放寫鎖之前要獲取讀鎖呢?
如果當(dāng)前的線程A在修改完cache中的數(shù)據(jù)后,沒有獲取讀鎖而是直接釋放了寫鎖;假設(shè)此時(shí)另一個(gè)線程B 獲取了寫鎖并修改了數(shù)據(jù),那么線程A無法感知到數(shù)據(jù)已被修改,但線程A還應(yīng)用了緩存數(shù)據(jù),所以就可能出現(xiàn)數(shù)據(jù)錯(cuò)誤
如果遵循鎖降級(jí)的步驟,線程A 在釋放寫鎖之前獲取讀鎖,那么線程B在獲取寫鎖時(shí)將被阻塞,直到線程A完成數(shù)據(jù)處理過程,釋放讀鎖,從而保證數(shù)據(jù)的可見性
那問題又來了:
使用寫鎖一定要降級(jí)嗎?
如果你理解了上面的問題,相信這個(gè)問題已經(jīng)有了答案。假如線程A修改完數(shù)據(jù)之后, 經(jīng)過耗時(shí)操作后想要再使用數(shù)據(jù)時(shí),希望使用的是自己修改后的數(shù)據(jù),而不是其他線程修改后的數(shù)據(jù),這樣的話確實(shí)是需要鎖降級(jí);如果只是希望最后使用數(shù)據(jù)的時(shí)候,拿到的是最新的數(shù)據(jù),而不一定是自己剛修改過的數(shù)據(jù),那么先釋放寫鎖,再獲取讀鎖,然后使用數(shù)據(jù)也無妨
在這里我要額外說明一下你可能存在的誤解:
如果已經(jīng)釋放了讀鎖再獲取寫鎖不叫鎖的升級(jí)
如果已經(jīng)釋放了寫鎖在獲取讀鎖也不叫鎖的降級(jí)
相信你到這里也理解了鎖的升級(jí)與降級(jí)過程,以及他們被允許或被禁止的原因了
總結(jié)
本文主要說明了 ReentrantReadWriteLock 是如何應(yīng)用 state 做位拆分實(shí)現(xiàn)讀/寫兩種同步狀態(tài)的,另外也通過源碼分析了讀/寫鎖獲取同步狀態(tài)的過程,最后又了解了讀寫鎖的升級(jí)/降級(jí)機(jī)制,相信到這里你對(duì)讀寫鎖已經(jīng)有了一定的理解。
?
靈魂追問
讀鎖也沒修改數(shù)據(jù),還允許共享式獲取,那還有必要設(shè)置讀鎖嗎?
在分布式環(huán)境中,你是如何保證緩存數(shù)據(jù)一致性的呢?
當(dāng)你打開看ReentrantReadWriteLock源碼時(shí),你會(huì)發(fā)現(xiàn),WriteLock 中可以使用 Condition,但是ReadLock 使用Condition卻會(huì)拋出UnsupportedOperationException,這是為什么呢?
參考
Java 并發(fā)實(shí)戰(zhàn)
Java 并發(fā)編程的藝術(shù)
https://www.jianshu.com/p/58697bb2243e
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
新人創(chuàng)作打卡挑戰(zhàn)賽發(fā)博客就能抽獎(jiǎng)!定制產(chǎn)品紅包拿不停!總結(jié)
以上是生活随笔為你收集整理的搞定ReentrantReadWriteLock 几道小小数学题就够了的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java web学习(一)Servlet
- 下一篇: 全面容器化:阿里5年带给我的最大收获