显式锁Lock的集大成之作,最细节教程
顯式鎖是什么?
我們一般喊synchronized就叫synchronized。其實(shí)synchronized又被稱為隱式鎖,但我們就愛喊它synchronized。而顯式鎖就是我們一般說的Lock鎖,但大家就是愛叫它顯式鎖。大概是Lock很容易和別的單詞搞混吧?但無(wú)論如何,顯式鎖就是說的Lock鎖。
那么Lock為啥要叫它顯式鎖呢?八竿子打不著一邊我很難記住啊!
我們來(lái)看一下Lock加鎖的范式:
如上圖,我們注意到,lock.unlock();是被放入 finally 代碼塊里的,這是為了保證出現(xiàn)異常時(shí),鎖依然能被釋放掉,避免死鎖的產(chǎn)生。順便提一下,我們的synchronized方法或synchronized代碼塊中的代碼,在執(zhí)行期間發(fā)生異常,變會(huì)自動(dòng)釋放鎖,因此沒有顯示的退出(unlock)。
Lock是一個(gè)接口,提供了無(wú)條件的、可輪詢的、定時(shí)的、可中斷的鎖獲取操作,所有的加鎖和解鎖操作方法都是顯示的(必須得寫出來(lái)),因而稱為顯式鎖。這下印象深刻了吧。
同時(shí),我們還要注意到,加鎖的過程:lock.lock();,并沒有放在 try 代碼塊內(nèi),而且你會(huì)發(fā)現(xiàn),JDK 文檔中很多使用 lock 的地方都是將加鎖過程:lock.lock();放在了 try 的外部。
lock.lock()放在try語(yǔ)句中的后果
博主在瀏覽一下網(wǎng)上的技術(shù)博客時(shí),發(fā)現(xiàn)竟然有人將lock.lock();加在了try語(yǔ)句內(nèi),如下面這種格式:
這樣做合適么?答案是肯定不合適。
假如我們?cè)趖ry語(yǔ)句和lock.lock();之間發(fā)生了異常,或者直接在獲取鎖的時(shí)候發(fā)生異常。那么就會(huì)在未成功執(zhí)行l(wèi)ock.lock();時(shí),執(zhí)行finally代碼塊的lock.unlock();去釋放鎖。可是此時(shí)我們并沒有獲取鎖,直接執(zhí)行釋放鎖會(huì)出現(xiàn)問題么?這個(gè)問題,我們留到后文的AQS中繼續(xù)探討。
synchronized和lock的區(qū)別
那么此處就對(duì)我們的隱式鎖和顯式鎖進(jìn)行一下對(duì)比:
出身,層次不同
從synchronized和lock的出身(原始的構(gòu)成)來(lái)看看兩者的不同:
- synchronized : Java中的關(guān)鍵字,是由JVM來(lái)維護(hù)的。是JVM層面的鎖。
- Lock:是JDK5以后才出現(xiàn)的具體的類。使用lock是調(diào)用對(duì)應(yīng)的API。是API層面的鎖。
synchronized 是底層是通過monitorenter進(jìn)行加鎖(底層是通過monitor對(duì)象來(lái)完成的,其中的wait/notify等方法也是依賴于monitor對(duì)象的。只有在同步塊或者是同步方法中才可以調(diào)用wait/notify等方法的。因?yàn)橹挥性谕綁K或者是同步方法中,JVM才會(huì)調(diào)用monitory對(duì)象的);通過monitorexit來(lái)退出鎖的。
而lock是通過調(diào)用對(duì)應(yīng)的API方法來(lái)獲取鎖和釋放鎖的。
通過反編譯的結(jié)果,我們可以看出synchronized和lock的區(qū)別。
用總結(jié)漫威電影的一句話來(lái)概述這兩個(gè)鎖的區(qū)別:窮人靠變異,富人裝備。synchronized自打娘胎里(JVM級(jí)別)就擁有不俗的實(shí)例,而lock則是在后期不斷通過裝備(類級(jí)別的封裝)展現(xiàn)出更多姿多彩的能力。
使用方式不同
這個(gè)在我們文章的頭部已經(jīng)介紹了,此處再給出以下用法的對(duì)比:
切記使用lock的時(shí)候,加鎖放在try外面,解鎖放在finally中。
換到漫威電影里,我們可以類比為:蜘蛛俠(synchronized)每次行動(dòng)前,帶個(gè)頭套就出去干了。而鋼鐵俠(lock)一定需要后期頻繁的給裝甲進(jìn)行充電,保養(yǎng)等護(hù)理事宜。
等待是否可以被打斷
首先,synchronized是不可中斷的(網(wǎng)上常說的一個(gè)不怎么規(guī)范的說法)。除非拋出異常或者正常運(yùn)行完成。
這里可能有些容易混淆。我們的線程Thread類不是提供了一個(gè)interrupt方法來(lái)中斷線程么?憑啥說synchronized是不可中斷的?
其實(shí)就是這個(gè)說法容易誤導(dǎo)我們,實(shí)際上是synchronized在阻塞狀態(tài)中是不可被打斷的。
我們知道,當(dāng)多個(gè)線程去訪問同一個(gè)synchronized對(duì)象鎖資源時(shí),一次只能有一個(gè)線程獲取到鎖,其他的線程此時(shí)就會(huì)進(jìn)入阻塞狀態(tài),直到搶到鎖資源的線程執(zhí)行完畢時(shí),這些阻塞中的線程才會(huì)被喚醒,去重新爭(zhēng)搶鎖。而這些正在阻塞中,尚未被喚醒的鎖資源,我們是無(wú)法將它們從阻塞中進(jìn)行打斷的。
而lock可以中斷,也是針對(duì)這些等待鎖資源的線程而言的。中斷的方式有以下兩種:
這里就不拿漫威的例子來(lái)講了。我們的大人常說我們談戀愛時(shí)不要在一棵樹上吊死就是這個(gè)打斷機(jī)制的原理。synchronized鎖在愛上了一個(gè)女孩兒,但這個(gè)女孩兒嫁給了別的男人,于是它選擇終其一生去等待她。而lock鎖就比較聽大人的話,更加的靈活。知道等待一段時(shí)間無(wú)法讓心愛的女孩兒回心轉(zhuǎn)意,就放棄了這一棵小樹,回過頭來(lái)就能發(fā)現(xiàn)一片森林。
當(dāng)然,并不是說lock就比synchronized好了。具體在程序中,用哪種鎖來(lái)處理問題,其實(shí)各有千秋。我們作為開發(fā)人員要根據(jù)不同的場(chǎng)景,選擇最適合處理這個(gè)業(yè)務(wù)場(chǎng)景的鎖。
公平與非公平的選擇權(quán)利不同
- synchronized:非公平鎖。
- lock:可以主動(dòng)選擇公平和非公平。可以在其構(gòu)造方法中進(jìn)行設(shè)置。默認(rèn)是非公平的(false),可以主動(dòng)設(shè)置為公平的(true)。
這個(gè)繼續(xù)用漫威電影來(lái)講。蜘蛛俠從小出身貧苦,因此出去和大家吃飯只能搶著吃。而鋼鐵俠成天和蜘蛛俠混一起,體驗(yàn)平民生活,因此也默認(rèn)是搶著吃飯。但同時(shí)他也可以選擇去上流環(huán)境中,大家井然有序的排隊(duì)打飯。
當(dāng)然,也可以看出,非公平(搶占式)鎖往往伴隨更高的效率(具體原因我們放在AQS中細(xì)講),而公平(非搶占式)鎖往往會(huì)降低效率,卻可以保證線程根據(jù)搶鎖的時(shí)間進(jìn)行排序執(zhí)行。
喚醒線程的粒度不同
- synchronized
不能精確喚醒線程。要么隨機(jī)喚醒一個(gè)線程;要么是喚醒所有等待的線程。 - Lock
用(condition)來(lái)實(shí)現(xiàn)分組喚醒需要喚醒的線程,可以精確的喚醒。
性能比較
JDK1.5(lock強(qiáng)于synchronized)
synchronized是托管給JVM執(zhí)行的,而lock是java寫的控制鎖的代碼。在Java1.5中,synchronized的性能是低效的,因?yàn)檫@是一個(gè)重量級(jí)鎖,需要調(diào)用操作接口,導(dǎo)致了有可能加鎖消耗的系統(tǒng)時(shí)間比加鎖之外的操作還多,相比之下,使用Java提供的Lock對(duì)象,性能就會(huì)高一些。
JDK1.6以及之后(官方推薦synchronized)
到了Java1.6之后,發(fā)生了變化。synchronized在語(yǔ)義很清晰并進(jìn)行了許多優(yōu)化。有適應(yīng)自旋,鎖消除,鎖粗化,輕量級(jí)鎖,偏向鎖等等,導(dǎo)致Java1.6中synchronized的性能并不比lock差。官方也表示,他們更支持使用synchronized,在未來(lái)的版本中還有優(yōu)化余地。
因此,現(xiàn)在大家最常用的可能都是Java1.8或者更高級(jí)的版本了,lock現(xiàn)在可以被我們當(dāng)做一個(gè)具有更多功能的一個(gè)鎖。當(dāng)業(yè)務(wù)不需要這些功能實(shí)現(xiàn)的時(shí)候,我們就盡量的選擇synchronized來(lái)加鎖。
Lock的常用API
void lock();
拿不到鎖就不罷休,不然一直block(阻塞)。和synchronized一樣的效果。而之后的方法都可以看作是對(duì)synchronized鎖的增強(qiáng)
void lockInterruptibly();
可中斷地獲取鎖,和lock()方法的不同之處在于該方法會(huì)響應(yīng)中斷,即在鎖在還未獲取到,阻塞等待中也可以中斷當(dāng)前線程。
boolean tryLock();
立刻判斷是否能拿到鎖,拿到返回true,否則返回false。
比較灑脫,跟渣男一樣,和女孩說一句我愛你,要么在一起,要么就拜拜。
boolean tryLock(long time,TimeUnit unit);
超時(shí)獲取鎖,當(dāng)線程在以下三種情況會(huì)返回:
void unlock();
釋放鎖
Lock的重要實(shí)現(xiàn)類
我們得Lock只是一個(gè)接口,要想具體干活兒,我們還得分析它旗下的子類們:
ReentrantLock(可重入鎖)
鎖的可重入
ReentrantLock翻譯過來(lái)為可重入鎖,它的可重入性表現(xiàn)在同一個(gè)線程可以多次獲得鎖,而不同線程依然不可多次獲得鎖,以線程作為鎖的分界線。最常見的場(chǎng)景就是遞歸,一個(gè)遞歸頻繁的調(diào)用一個(gè)帶鎖的方法,此時(shí)作為可重復(fù)鎖,對(duì)于該線程是可以重復(fù)的累積獲取鎖的。當(dāng)然,在遞歸結(jié)束后,我們也需要再次循環(huán)的將鎖釋放依次釋放掉,以達(dá)到我們所說的效果。具體的流程我們依舊放在AQS中討論。
鎖的公平與非公平
ReentrantLock還分為公平鎖和非公平鎖(好家伙,這一個(gè)類占了倆名兒),公平鎖保證等待時(shí)間最長(zhǎng)的線程將優(yōu)先獲得鎖,而非公平鎖并不會(huì)保證多個(gè)線程獲得鎖的順序,但是非公平鎖的并發(fā)性能表現(xiàn)更好,至于性能的分析問題,我們還是放在AQS中去說。ReentrantLock默認(rèn)使用非公平鎖。
ReentrantReadWriteLock(讀寫鎖)
之前提到的鎖(synchronized和ReentrantLock)都是排他鎖,這些鎖在同一時(shí)刻只運(yùn)行一個(gè)線程進(jìn)行訪問讀寫鎖維護(hù)了兩把鎖,一個(gè)讀鎖和一個(gè)寫鎖。通過分離讀鎖和寫鎖,使得并發(fā)性相比一般的排他鎖有了很大提升。
- 當(dāng)讀線程搶到鎖:同一時(shí)刻可以允許多個(gè)讀線程訪問,并阻塞寫線程。
- 當(dāng)寫線程搶到鎖:寫鎖就是一個(gè)排它鎖,所有讀線程和其他寫線程均被阻塞。
讀寫鎖比互斥鎖允許對(duì)于共享數(shù)據(jù)更大程度的并發(fā)。每次只能有一個(gè)寫線程,但是同時(shí)可以有多個(gè)線程并發(fā)地讀數(shù)據(jù)。ReadWriteLock適用于讀多寫少的并發(fā)情況。
讀寫鎖造成的寫線程饑餓
其實(shí),博主一直認(rèn)為讀寫鎖是一個(gè)很矛盾的鎖。因?yàn)樗脑O(shè)計(jì)原理(維護(hù)了兩把鎖),導(dǎo)致它的應(yīng)用場(chǎng)景就是讀多寫少的場(chǎng)景。而讀多寫少的場(chǎng)景必然會(huì)造成另一個(gè)問題,就是寫線程饑餓。
什么是線程饑餓呢?我們來(lái)舉一個(gè)例子。
假設(shè)我們現(xiàn)在有A,B兩個(gè)線程讀,一個(gè)C線程寫。假設(shè)A線程在快執(zhí)行完畢的時(shí)候,B線程繼續(xù)開始進(jìn)行讀操作。當(dāng)B線程快執(zhí)行完畢時(shí),A線程又繼續(xù)開始讀操作。我們發(fā)現(xiàn),只要在所有讀線程結(jié)束前,任何一個(gè)新出現(xiàn)的讀線程就可以繼續(xù)維持讀鎖的使用權(quán)。這種概率伴隨著讀線程的增多而增大,因此,讀多寫少的情況下適合讀寫鎖是勿用質(zhì)疑的,但是寫線程饑餓可能讓我們的讀線程長(zhǎng)時(shí)間無(wú)法獲取到最新的數(shù)據(jù)。
那么,有什么方法可以解決我們的線程饑餓呢?
公平鎖處理線程饑餓
之前說到公平鎖的執(zhí)行效率會(huì)比非公平鎖低下,也許讀者就會(huì)思考,那么有什么場(chǎng)景下會(huì)寧可犧牲效率,也要使用公平鎖呢?瞧,這里就是一個(gè)應(yīng)用場(chǎng)景。
公平鎖的實(shí)現(xiàn)原理就是讓根據(jù)線程的訪問先后順序,對(duì)它們進(jìn)行排序。再加上讀線程之間是不互斥的,因此讀線程過多的場(chǎng)景下并不會(huì)讓我們的維持的隊(duì)列堵塞,當(dāng)遇到一個(gè)寫線程時(shí),隊(duì)列才會(huì)堵塞,等待最后一個(gè)讀線程執(zhí)行完畢后,就可以開始執(zhí)行我們的寫線程了。這個(gè)優(yōu)化就保證我們的寫線程的執(zhí)行時(shí)效性問題。
但是公平鎖天然的就會(huì)比非公平鎖效率底下很多(具體原因在AQS中詳細(xì)說明),因此此策略是以犧牲系統(tǒng)吞吐量為代價(jià)的。
StampedLock處理線程饑餓(簡(jiǎn)單理解)
StampedLock是Java8引入的一種新的所機(jī)制,簡(jiǎn)單的理解,可以認(rèn)為它是讀寫鎖的一個(gè)改進(jìn)版本,它提供了一種樂觀的讀策略。
StampedLock 的樂觀讀允許一個(gè)寫線程獲取寫鎖,所以不會(huì)導(dǎo)致所有寫線程阻塞,也就是當(dāng)讀多寫少的時(shí)候,寫線程有機(jī)會(huì)獲取寫鎖,減少了線程饑餓的問題,吞吐量大大提高。
這里可能你就會(huì)有疑問,竟然同時(shí)允許多個(gè)樂觀讀和一個(gè)先線程同時(shí)進(jìn)入臨界資源操作,那讀取的數(shù)據(jù)可能是錯(cuò)的怎么辦?
是的,樂觀讀不能保證讀取到的數(shù)據(jù)是最新的,所以將數(shù)據(jù)讀取到局部變量的時(shí)候需要通過 lock.validate(stamp) 這個(gè)標(biāo)志位校驗(yàn)是否被寫線程修改過,若是修改過則需要上悲觀讀鎖,再重新讀取數(shù)據(jù)到局部變量。可以看到其實(shí)就是一個(gè)CAS操作。
同時(shí)由于樂觀讀并不是鎖,所以沒有線程喚醒與阻塞導(dǎo)致的上下文切換,性能更好。
讀寫鎖之鎖降級(jí)(寫鎖可降級(jí))
鎖降級(jí)是指把持住當(dāng)前擁有的寫鎖的同時(shí),再獲取到讀鎖,隨后釋放寫鎖的過程。
注意:如果當(dāng)前線程擁有寫鎖,然后將其釋放,最后再獲取到讀鎖,這種分段完成的過程不能稱之為鎖降級(jí)。
很多人對(duì)這個(gè)概念都會(huì)忽略,即時(shí)略有了解鎖降級(jí)的朋友,也可能光知道上面的概念而已。本文讓你徹底悟透這個(gè)技術(shù)。
首先,寫鎖降級(jí)為讀鎖,并不是我們的寫鎖自帶的功能!!!重要的事兒說三遍,因?yàn)椴┲髯约涸谶@個(gè)點(diǎn)上誤解了很久。就像打游戲一樣,寫鎖降級(jí)完全是一個(gè)主動(dòng)釋放技能,而不是人家寫鎖自帶的被動(dòng)技能!那么寫鎖降級(jí)技術(shù)解決了什么問題呢?那就是為了防止臟讀,因此設(shè)置一個(gè)讀鎖在當(dāng)前線程徹底執(zhí)行結(jié)束前,阻塞其他線程獲取寫鎖修改數(shù)據(jù)。
純語(yǔ)言描述可能不易理解,我們來(lái)看一下下面的例子,理解臟讀產(chǎn)生的原因:
? ?? ???我們發(fā)現(xiàn),只加寫鎖,并且作用域不夠大的話,就可能會(huì)出現(xiàn)我們描述的這種情況。對(duì)于對(duì)數(shù)據(jù)精準(zhǔn)度較高的系統(tǒng)而言,這種情況就不大友好了。那么寫鎖降級(jí)是怎么解決這個(gè)臟讀的問題的呢?
首先先用代碼揭開一下鎖降級(jí)的真面目。
我們可以看到,所謂的鎖降級(jí),并不是真正意義的降級(jí),而是在保持寫鎖的過程中,可以繼續(xù)獲取讀鎖,當(dāng)釋放寫鎖時(shí),讀鎖依舊保持著,以達(dá)到"表面降級(jí)"的目的。
我們來(lái)看看鎖降級(jí)具體是如何操作的:
可以看到,實(shí)際上還是靈活運(yùn)用了讀寫鎖的機(jī)制,讓其他寫線程一直阻塞到本線程結(jié)束后再繼續(xù)擁有爭(zhēng)奪鎖的資格,以解決臟讀的問題。
當(dāng)然,這種解決方案會(huì)極大的影響業(yè)務(wù)的更新數(shù)據(jù)的時(shí)效性。
那你可能會(huì)想到,如果是可見性問題,那么voliate關(guān)鍵字是否可以解決可見性問題呢?這里千萬(wàn)不能混淆,因?yàn)檫@里依舊會(huì)出現(xiàn)數(shù)據(jù)原子性的問題。如果本身不用降級(jí)會(huì)只會(huì)讓我們讀到舊版的數(shù)據(jù),那么想用voliate替代鎖降級(jí),直接會(huì)造成數(shù)據(jù)錯(cuò)亂的問題。
鎖降級(jí)的好處
繞這么大半天,我們直接把鎖的作用域加大,不也可以保證數(shù)據(jù)的強(qiáng)一致性?但是用了鎖降級(jí)技術(shù),可以在我們上述代碼等待10s的過程中不阻塞那些需要獲取讀鎖的線程。因此鎖降級(jí)技術(shù)是一個(gè)非常好用卻又容易被我們忽略的技術(shù)。
Lock的分組喚醒機(jī)制Condition
回憶 synchronized 關(guān)鍵字,它配合 Object 的 wait()、notify() 系列方法可以實(shí)現(xiàn)等待/通知模式。對(duì)于 Lock,通過 Condition 也可以實(shí)現(xiàn)等待/通知模式。Condition是在java 1.5中才出現(xiàn)的,它用來(lái)替代傳統(tǒng)的Object的wait()、notify()實(shí)現(xiàn)線程間的協(xié)作,相比使用Object的wait()、notify(),使用Condition的await()、signal()這種方式實(shí)現(xiàn)線程間協(xié)作更加安全和高效。因此通常來(lái)說比較推薦使用Condition,阻塞隊(duì)列實(shí)際上是使用了Condition來(lái)模擬線程間協(xié)作。
Condition的API介紹
Condition是個(gè)接口,基本的方法就是await()和signal()方法;
Condition依賴于Lock接口,生成一個(gè)Condition的基本代碼是lock.newCondition();
調(diào)用Condition的await()和signal()方法,都必須在lock保護(hù)之內(nèi),就是說必須在lock.lock()和lock.unlock之間才可以使用。下面先列舉三個(gè)最常用的方法:
- Conditon中的await()對(duì)應(yīng)Object的wait();
- Condition中的signal()對(duì)應(yīng)Object的notify();
- Condition中的signalAll()對(duì)應(yīng)Object的notifyAll()。
同時(shí),我們的根據(jù)我們的java線程的超時(shí)等待狀態(tài),共提供了以下這些方法:
- await() :造成當(dāng)前線程在接到信號(hào)或被中斷之前一直處于等待狀態(tài)。
- await(long time, TimeUnit unit) :造成當(dāng)前線程在接到信號(hào)、被中斷或到達(dá)指定等待時(shí)間之前一直處于等待狀態(tài)
- awaitNanos(long nanosTimeout) :造成當(dāng)前線程在接到信號(hào)、被中斷或到達(dá)指定等待時(shí)間之前一直處于等待狀態(tài)。返回值表示剩余時(shí)間,如果在nanosTimesout之前喚醒,那么返回值 = nanosTimeout - 消耗時(shí)間,如果返回值 <= 0 ,則可以認(rèn)定它已經(jīng)超時(shí)了。
- awaitUninterruptibly() :造成當(dāng)前線程在接到信號(hào)之前一直處于等待狀態(tài)。【注意:該方法對(duì)中斷不敏感】。
- awaitUntil(Date deadline) :造成當(dāng)前線程在接到信號(hào)、被中斷或到達(dá)指定最后期限之前一直處于等待狀態(tài)。如果沒有到指定時(shí)間就被通知,則返回true,否則表示到了指定時(shí)間,返回返回false。
- signal() :喚醒一個(gè)等待線程。該線程從等待方法返回前必須獲得與Condition相關(guān)的鎖。
- signal()All :喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關(guān)的鎖。
分組喚醒Condition如何高效
synchronized實(shí)現(xiàn)生產(chǎn)消費(fèi)模式的弊端
我們來(lái)說說,我們直接用原本synchronized自帶的等待喚醒機(jī)制去實(shí)現(xiàn)生產(chǎn)消費(fèi)模式存在什么弊端。
首先來(lái)看看一個(gè)生產(chǎn)者與消費(fèi)者多對(duì)多的例子:
存值模板
取值模板
線程任務(wù)類
調(diào)用者
消費(fèi),生產(chǎn)線程各創(chuàng)建5個(gè)。
問題分析
稍微看一下我們的邏輯,可知每當(dāng)消費(fèi)者沒有數(shù)據(jù)就掛起,喚醒生產(chǎn)者。而生產(chǎn)者僅僅生產(chǎn)一個(gè)數(shù)據(jù)就進(jìn)行掛起,喚醒消費(fèi)者進(jìn)行消費(fèi)。但是僅僅這種簡(jiǎn)單的單數(shù)據(jù)生產(chǎn)消費(fèi)者模式,我們依舊需要使用notifyAll()喚醒所有線程,這是為什么?答案是:我們的存值線程與取值線程用的都是同一把鎖。如果僅僅喚醒一個(gè)線程,我們無(wú)法保證消費(fèi)完了,下次喚醒的一定是生產(chǎn)者。生產(chǎn)者生產(chǎn)完了,下次喚醒的一定是消費(fèi)者。
運(yùn)氣不好的情況下,假設(shè)我們生產(chǎn)線程生產(chǎn)完畢,連續(xù)隨機(jī)又喚醒10次生產(chǎn)線程,那么此時(shí)這10次生產(chǎn)線程必定都是無(wú)法生產(chǎn)的,只能經(jīng)過判斷重新掛起,直到消費(fèi)線程搶占到鎖,程序才能繼續(xù)正常執(zhí)行。兒這中間就進(jìn)行了10次無(wú)意義的上下文切換,是非常低效的。
我們來(lái)看一下運(yùn)行結(jié)果:
從運(yùn)行結(jié)果中可知,我畫的紅線部分的線程都是隨機(jī)喚醒后,無(wú)法成功執(zhí)行任務(wù)的線程。在while循環(huán)中判斷不符合執(zhí)行條件后,重新進(jìn)入等待狀態(tài)。這些步驟完全是浪費(fèi)時(shí)間的。
Condition保證生產(chǎn)消費(fèi)模式的高效
成員變量
存值模板
取值模板
線程任務(wù)類
調(diào)用者
消費(fèi),生產(chǎn)線程各創(chuàng)建5個(gè)。
結(jié)果分析
結(jié)果已經(jīng)變得很井井有條了,固定的消費(fèi)一個(gè)會(huì)生產(chǎn)一個(gè),生產(chǎn)一個(gè)消費(fèi)一個(gè)。
我們發(fā)現(xiàn),我們將消費(fèi)者和生產(chǎn)者進(jìn)行了分流。在我們的產(chǎn)品固定只有一個(gè)(list.size()=1)的情況下,我們甚至可以使用signal()來(lái)隨機(jī)喚醒一個(gè)消費(fèi)/生產(chǎn)線程來(lái)執(zhí)行任務(wù)。
當(dāng)然,如果我們產(chǎn)品數(shù)大于一個(gè)(list.size()>1)的情況下,我們依舊需要使用signalAll()來(lái)批量喚醒線程來(lái)?yè)屨紙?zhí)行任務(wù)。但是在需要生產(chǎn)的情況下,依舊會(huì)排除掉所有的消費(fèi)者。需要消費(fèi)的時(shí)候也會(huì)排除掉所有的生產(chǎn)者,從而減小上下文切換的概率,以達(dá)到提升效率的目的。
一個(gè)鎖兩個(gè)Condition能否被兩把鎖替代
可能這個(gè)點(diǎn)是作者的奇思妙想。但在此處也提一下:
還是之前的例子。做以下修改:
成員遍變量
生成兩個(gè)鎖,并且分別持有一個(gè)消費(fèi)者Condition和生產(chǎn)者Condition。
存值模板
注意,在生產(chǎn)者鎖范圍內(nèi),我加了一個(gè)消費(fèi)者的Condition喚醒。
取值模板
與上面相反,在消費(fèi)者鎖范圍內(nèi),我加了一個(gè)生產(chǎn)者的Condition喚醒。
線程任務(wù)類
調(diào)用者
依舊創(chuàng)建5個(gè)線程運(yùn)行,我們來(lái)看看結(jié)果
結(jié)果分析
結(jié)果發(fā)生了異常,在網(wǎng)上查到了該異常的解釋:
拋出該異常表明某一線程已經(jīng)試圖等待對(duì)象的監(jiān)視器,或者試圖通知其他正在等待對(duì)象的監(jiān)視器,然而本身沒有指定的監(jiān)視器的線程。
也就是說,我們一把鎖內(nèi)只能使用本鎖內(nèi)的Condition。
更簡(jiǎn)單靈活的等待喚醒工具類LockSupport
LockSupport是什么
LockSupport是一個(gè)編程工具類,主要是為了阻塞和喚醒線程用的。它所有的方法都是靜態(tài)方法,可以讓線程在任意位置阻塞,也可以在任意位置喚醒。
它的內(nèi)部其實(shí)兩類主要的方法:park(停車阻塞線程)和unpark(啟動(dòng)喚醒線程)。
注意上面的123方法,都有一個(gè)blocker,這個(gè)blocker是用來(lái)記錄線程被阻塞時(shí)被誰(shuí)阻塞的。用于線程監(jiān)控和分析工具來(lái)定位原因的。
現(xiàn)在我們知道了LockSupport是用來(lái)阻塞和喚醒線程的,而且之前相信我們都知道wait/notify也是用來(lái)阻塞和喚醒線程的,那么它相比,LockSupport有什么優(yōu)點(diǎn)呢?
與wait/notify對(duì)比
wait/notify機(jī)制是基于鎖機(jī)制的等待喚醒。那么我們這個(gè)工具類能強(qiáng)大到什么地步呢?
上面的代碼中,MyThread線程中,不需要加鎖,便可以實(shí)現(xiàn)線程的中斷。而當(dāng)其他線程需要對(duì)其進(jìn)行喚醒時(shí),僅僅需要將該線程對(duì)象作為參數(shù),調(diào)用LockSupport.unpark(線程對(duì)象),即可讓程序繼續(xù)運(yùn)行。
與wait/notfy的區(qū)別具體有以下兩點(diǎn):
notify只能隨機(jī)選擇一個(gè)線程喚醒,無(wú)法喚醒指定的線程,unpark卻可以喚醒一個(gè)指定的線程
總結(jié)
以上是生活随笔為你收集整理的显式锁Lock的集大成之作,最细节教程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 树莓派新手使用iobroker日志三(米
- 下一篇: 关于SYSTICK延时函数的两个小疑问