- Java多線程編程實(shí)戰(zhàn)指南:設(shè)計(jì)模式篇(第2版)
- 黃文海
- 2332字
- 2021-10-15 19:24:58
4.4 Guarded Suspension模式的評價(jià)與實(shí)現(xiàn)考量
Guarded Suspension模式使應(yīng)用程序避免了樣板式代碼。Guarded Suspension模式的Blocker參與者所實(shí)現(xiàn)的線程掛起與喚醒功能固然可以由應(yīng)用代碼直接使用wait/notify或者java.util.concurrent.locks.Condition來實(shí)現(xiàn),但是這里面涉及幾個比較容易犯錯的重要技術(shù)細(xì)節(jié)(在下面的內(nèi)容中會提到)。這些細(xì)節(jié)如果散落在應(yīng)用代碼中,則會增加出錯的概率。另外,應(yīng)用直接編寫代碼來正確實(shí)現(xiàn)這些技術(shù)細(xì)節(jié)往往會導(dǎo)致許多樣板式代碼。這不僅增加了代碼編寫的工作量,也增加了出錯的概率。相反,Guarded Suspension模式的Blocker參與者封裝了這些易錯的技術(shù)細(xì)節(jié),從而減少了應(yīng)用代碼的編寫工作量和出錯的概率。
使關(guān)注點(diǎn)分離(Separation of Concern)。Guarded Suspension模式中的各個參與者分別只關(guān)注本模式所要解決的問題中的一個方面,各個參與者的職責(zé)是高度內(nèi)聚(Cohesive)的。這使得Guarded Suspension模式便于理解和應(yīng)用,其實(shí)現(xiàn)代碼也便于閱讀和維護(hù)。應(yīng)用開發(fā)人員只需要根據(jù)應(yīng)用的需要實(shí)現(xiàn)GuardedObject、ConcretePredicate和ConcreteGuardedAction這幾個必須由應(yīng)用實(shí)現(xiàn)的參與者,而其他參與者的實(shí)現(xiàn)都是可復(fù)用的。
可能增加Java虛擬機(jī)垃圾回收的負(fù)擔(dān)。為了使GuardedAction實(shí)例的call方法能夠訪問受保護(hù)方法guardedMethod的參數(shù),我們需要利用閉包(Closure)。因此,GuardedAction實(shí)例可能是在受保護(hù)方法中創(chuàng)建的。這意味著,在每次調(diào)用受保護(hù)方法的時候,都會有一個新的GuardedAction實(shí)例被創(chuàng)建。而這會增加Java虛擬機(jī)內(nèi)存池Eden區(qū)域內(nèi)存的占用,從而可能增加Java虛擬機(jī)垃圾回收的負(fù)擔(dān)。如果應(yīng)用所在Java虛擬機(jī)的內(nèi)存池Eden區(qū)域的空間比較小,則需要特別注意創(chuàng)建GuardedAction實(shí)例可能導(dǎo)致的垃圾回收負(fù)擔(dān)。
可能增加上下文切換(Context Switch)。嚴(yán)格來說,這點(diǎn)與Guarded Suspension模式本身無關(guān)。只不過,不管如何實(shí)現(xiàn)Guarded Suspension模式,只要其中涉及線程的暫掛和喚醒,就會引起上下文切換。如果頻繁出現(xiàn)受保護(hù)方法被調(diào)用時保護(hù)條件不成立,那么受保護(hù)方法的執(zhí)行線程就會頻繁地被暫掛和喚醒,從而導(dǎo)致頻繁的上下文切換。過于頻繁的上下文切換會過多地消耗系統(tǒng)的CPU時間,從而降低系統(tǒng)的處理能力。
下面將介紹Blocker實(shí)現(xiàn)類中封裝的幾個易錯的重要技術(shù)細(xì)節(jié)。
4.4.1 內(nèi)存可見性和鎖泄漏(Lock Leak)
保護(hù)條件中涉及的變量牽涉讀線程和寫線程進(jìn)行共享訪問。受保護(hù)方法的執(zhí)行線程是讀線程,它讀取這些變量以判斷保護(hù)條件是否成立。而寫線程是受保護(hù)對象實(shí)例的stateChanged方法的執(zhí)行線程,它會更改這些變量的值。因此,對保護(hù)條件涉及的變量的訪問應(yīng)該使用鎖進(jìn)行保護(hù),以保證對于寫線程對這些變量所做的更改,讀線程能夠“看到”相應(yīng)的值。從清單4-5中的代碼可以看出,這點(diǎn)已經(jīng)被封裝在Blocker實(shí)例的幾個方法中了。應(yīng)用代碼只需要在創(chuàng)建Blocker實(shí)例時在其構(gòu)造器中指定恰當(dāng)?shù)逆i實(shí)例即可。
ConditionVarBlocker類(代碼見清單4-5)為了保證保護(hù)條件中涉及的變量的內(nèi)存可見性而引入了ReentrantLock鎖。使用該鎖時需要注意臨界區(qū)中的代碼無論是執(zhí)行正常還是出現(xiàn)異常,進(jìn)入臨界區(qū)前獲得的鎖實(shí)例都應(yīng)該被釋放。否則,就會出現(xiàn)鎖泄漏現(xiàn)象:鎖對象被某個線程獲得,但永遠(yuǎn)不會被該線程釋放,導(dǎo)致其他線程無法獲得該鎖。為了避免鎖泄漏,使用ReentrantLock的臨界區(qū)代碼總是需要按照如下格式來編寫:

4.4.2 線程被過早地喚醒
ConditionVarBlocker類的callWithGuard方法對Condition實(shí)例的await方法的調(diào)用是放在一個while循環(huán)中的,而不是放在if語句中的。這是因?yàn)镃ondition實(shí)例的await方法使當(dāng)前線程暫掛后,其他線程雖然可以通過調(diào)用Condition實(shí)例的signal或者signalAll方法來喚醒被暫掛的線程,但是并不能保證此時保護(hù)條件就是成立的。因此,這個時候callWithGuard方法應(yīng)該繼續(xù)檢測保護(hù)條件,直到保護(hù)條件成立。
ConditionVarBlocker類是基于Condition接口實(shí)現(xiàn)的。由callWithGuard方法的簽名可知,一個Condition實(shí)例可以對應(yīng)多個受保護(hù)方法。假設(shè)某Condition的實(shí)例aCondition對應(yīng)多個保護(hù)條件predicateA和predicateB。當(dāng)某個線程threadA發(fā)現(xiàn)predicateA成立時,它調(diào)用了aCondition的notifyAll方法,那么此時所有被aCondition暫掛的線程都會被喚醒。這些被喚醒的線程中的某個線程threadB需要等待的保護(hù)條件是predicateB,顯然這時predicateB不一定成立。也就是,這時線程threadB被“過早”地喚醒了[5]。
導(dǎo)致線程被“過早”地喚醒的因素還有所謂的欺騙性喚醒(Spurious Wakeup),即在沒有其他任何線程調(diào)用Condition實(shí)例的notify/notifyAll方法的情況下,Condition實(shí)例的await方法就返回了。
因此,為了應(yīng)對被暫掛線程可能被“過早”地喚醒的情形,將callWithGuard方法對保護(hù)條件的檢測和對Condition實(shí)例的await方法的調(diào)用總是放在一個while循環(huán)而非if語句中。代碼樣板如下:

在應(yīng)用Guarded Suspension模式的時候還需要注意嵌套監(jiān)視器鎖死問題(Nested Monitor Lockout),具體介紹可見4.4.3節(jié)。
4.4.3 嵌套監(jiān)視器鎖死
ConditionVarBlocker實(shí)例的callWithGuard方法和signalAfter/signal/broadcastAfter/broadcast方法本身涉及由java.util.concurrent.locks.Lock實(shí)例實(shí)現(xiàn)的同步塊。如果這些方法又在另一個同步塊代碼中被調(diào)用,那么這就形成了嵌套同步。而嵌套同步可能產(chǎn)生鎖死的問題(類似死鎖)。下面看一個嵌套同步導(dǎo)致鎖死(即嵌套監(jiān)視器鎖死)的示例代碼(見清單4-6)。
清單4-6 嵌套監(jiān)視器鎖死示例代碼


如清單4-6所示的程序,如果我們將其xGuardedMethod方法和xStateChanged方法的synchronized關(guān)鍵字去掉,那么運(yùn)行該程序可以得到類似如下的輸出:

但是,如果保留xGuardedMethod方法和xStateChanged方法的synchronized關(guān)鍵字,那么上述程序運(yùn)行的時候,程序的輸出始終只有上面顯示的輸出的第1行。這說明xGuardedMethod方法一直沒有返回。
xGuardedMethod方法最終會調(diào)用java.util.concurrent.locks.Condition實(shí)例的await方法。Condition實(shí)例的await方法在其被調(diào)用之后且返回之前會釋放與其所屬的Condition實(shí)例所關(guān)聯(lián)的鎖,但是xGuardedMethod方法本身所獲得的鎖(即guardedMethod方法所屬的NestedMonitorLockoutExample實(shí)例)并沒有被釋放。而Condition實(shí)例的await方法要返回需要其他線程調(diào)用Condition實(shí)例的signal/signalAll方法。但是,調(diào)用xStateChanged方法所需獲得的鎖此時還是被xGuardedMethod方法的執(zhí)行線程所保留。這就意味著其他線程無法通過調(diào)用xStateChanged方法使得xGuardedMethod方法所調(diào)用的Condition實(shí)例的await方法返回,而這點(diǎn)又導(dǎo)致無法獲得xStateChanged方法調(diào)用時所需的鎖。這就形成了鎖死,如圖4-4所示。

圖4-4 嵌套監(jiān)視器鎖死的線程示例
從如圖4-4所示的調(diào)用棧(Call Stack)可知,線程Thread-0和線程Timer-0一直都處于這樣一個狀態(tài):前者擁有一個鎖,而后者等待獲取前者擁有的鎖,但是前者又等待后者喚醒自己,之后才會釋放其擁有的鎖。
如果我們能夠避免不必要的嵌套同步,那么嵌套監(jiān)視器鎖死的問題也就可以避免。ConditionVarBlocker的構(gòu)造器支持傳入一個java.util.concurrent.locks.Lock實(shí)例,這正是出于避免不必要的嵌套同步的考慮。該構(gòu)造器使得在ConditionVarBlocker調(diào)用Condition實(shí)例的相關(guān)方法時可以使用指定的Lock實(shí)例,而不是使用ConditionVarBlocker實(shí)例自己創(chuàng)建的Lock實(shí)例。
- C程序設(shè)計(jì)簡明教程(第二版)
- JavaScript全程指南
- Java面向?qū)ο笏枷肱c程序設(shè)計(jì)
- Vue.js 3.0源碼解析(微課視頻版)
- Mastering Python High Performance
- MySQL數(shù)據(jù)庫管理與開發(fā)實(shí)踐教程 (清華電腦學(xué)堂)
- INSTANT OpenNMS Starter
- Learning PHP 7
- Java程序員面試筆試寶典(第2版)
- 編程改變生活:用Python提升你的能力(進(jìn)階篇·微課視頻版)
- Getting Started with Python
- Julia High Performance(Second Edition)
- SSH框架企業(yè)級應(yīng)用實(shí)戰(zhàn)
- Office VBA開發(fā)經(jīng)典:中級進(jìn)階卷
- Developing RESTful Web Services with Jersey 2.0