java monitor
【個(gè)人筆記搬運(yùn)到博客系列】
一、monitor概念
在操作系統(tǒng)中,存在著semaphore和mutex,為了更好的編寫(xiě)并發(fā)程序,在mutex和semaphore基礎(chǔ)上,提出了更高層次的同步原語(yǔ),實(shí)際上,monitor屬于編程語(yǔ)言的范疇,具體的實(shí)現(xiàn)模式,不同的編程語(yǔ)言都有可能不一樣,C語(yǔ)言不支持monitor,而java支持monitor機(jī)制。
一個(gè)重要特點(diǎn)是,在同一時(shí)間,只有一個(gè)線(xiàn)程/進(jìn)程能進(jìn)入monitor所定義的臨界區(qū),這使得monitor能夠?qū)崿F(xiàn)互斥的效果。無(wú)法進(jìn)入monitor的臨界區(qū)的進(jìn)程/線(xiàn)程,應(yīng)該被阻塞,并且在適當(dāng)?shù)臅r(shí)候被喚醒。顯然,monitor作為一個(gè)同步工具,也應(yīng)該提供這樣管理線(xiàn)程/進(jìn)程的機(jī)制。
monitor這個(gè)機(jī)制之所以被稱(chēng)為:更高級(jí)的原語(yǔ),它不可避免的需要對(duì)外屏蔽這些機(jī)制,并且在內(nèi)部實(shí)現(xiàn)這些機(jī)制;
二、monitor基本元素
- 臨界區(qū)
- monitor對(duì)象和鎖
- 條件變量,以及定義在monitor對(duì)象上的wait,notify操作
使用monitor主要是為了互斥進(jìn)入臨界區(qū),為了能夠阻塞無(wú)法進(jìn)入臨界區(qū)的進(jìn)程,線(xiàn)程,需要一個(gè)monitor object來(lái)協(xié)助,這個(gè)object內(nèi)部會(huì)有相應(yīng)的數(shù)據(jù)結(jié)構(gòu),例如列表,用來(lái)保存被阻塞的線(xiàn)程;同時(shí)由于monitor機(jī)制本質(zhì)是基于mutex原語(yǔ)的,所以object必須維護(hù)一個(gè)基于mutex的鎖。
此外,為了在適當(dāng)?shù)臅r(shí)候能夠阻塞和喚醒 進(jìn)程/線(xiàn)程,還需要引入一個(gè)條件變量,這個(gè)條件變量用來(lái)決定什么時(shí)候是“適當(dāng)?shù)臅r(shí)候”,這個(gè)條件可以來(lái)自程序代碼的邏輯,也可以是在 monitor object 的內(nèi)部,總而言之,程序員對(duì)條件變量的定義有很大的自主性。不過(guò),由于 monitor object 內(nèi)部采用了數(shù)據(jù)結(jié)構(gòu)來(lái)保存被阻塞的隊(duì)列,因此它也必須對(duì)外提供兩個(gè) API 來(lái)讓線(xiàn)程進(jìn)入阻塞狀態(tài)以及之后被喚醒,分別是 wait 和 notify。
三、monitor在java中的實(shí)現(xiàn)
臨界區(qū):被synchronized關(guān)鍵字修飾的方法,代碼塊,就是monitor機(jī)制的臨界區(qū);
monitor object:在上述synchronized關(guān)鍵字被使用時(shí),往往需要指定一個(gè)對(duì)象與之關(guān)聯(lián),例如synchronized(this),總之,synchronized需要管理一個(gè)對(duì)象,這個(gè)對(duì)象就是monitor object。
Java 對(duì)象存儲(chǔ)在內(nèi)存中,分別分為三個(gè)部分,即對(duì)象頭、實(shí)例數(shù)據(jù)(對(duì)象體)和對(duì)齊填充,而在其對(duì)象頭中,保存了鎖標(biāo)識(shí);同時(shí),java.lang.Object 類(lèi)定義了 wait(),notify(),notifyAll() 方法,這些方法的具體實(shí)現(xiàn),依賴(lài)于一個(gè)叫 ObjectMonitor 模式的實(shí)現(xiàn),這是 JVM 內(nèi)部基于 C++ 實(shí)現(xiàn)的一套機(jī)制,基本原理如下所示:
以下以 Java 的 monitor 為例子,來(lái)講解 monitor 在 Java 中的實(shí)現(xiàn)方式:
在Java中,一個(gè)對(duì)象對(duì)應(yīng)了一個(gè)momitor對(duì)象,而synchronized關(guān)鍵字也需要關(guān)聯(lián)一個(gè)對(duì)象,這個(gè)對(duì)象需要天生就支持monitor,所以在Java中,可以就是Java 中的 java.lang.Object 類(lèi),便是滿(mǎn)足這個(gè)要求的對(duì)象,任何一個(gè) Java 對(duì)象都可以作為 monitor 機(jī)制的 monitor object。這也就是wait(),notify(),notifyAll(),是在Object類(lèi)中的原因。
四、monitor詳解
4.1、線(xiàn)程狀態(tài)
Java中線(xiàn)程中狀態(tài)可分為五種:New(新建狀態(tài)),Runnable(就緒狀態(tài)),Running(運(yùn)行狀態(tài)),Blocked(阻塞狀態(tài)),Dead(死亡狀態(tài))。
New:新建狀態(tài),當(dāng)線(xiàn)程創(chuàng)建完成時(shí)為新建狀態(tài),即new Thread(...),還沒(méi)有調(diào)用start方法時(shí),線(xiàn)程處于新建狀態(tài)。
Runnable:就緒狀態(tài),當(dāng)調(diào)用線(xiàn)程的的start()方法后,線(xiàn)程進(jìn)入就緒狀態(tài),等待CPU資源。處于就緒狀態(tài)的線(xiàn)程由Java運(yùn)行時(shí)系統(tǒng)的線(xiàn)程調(diào)度程序(thread scheduler)來(lái)調(diào)度。
Running:運(yùn)行狀態(tài),就緒狀態(tài)的線(xiàn)程獲取到CPU執(zhí)行權(quán)以后進(jìn)入運(yùn)行狀態(tài),開(kāi)始執(zhí)行run()方法。
Blocked:阻塞狀態(tài),線(xiàn)程沒(méi)有執(zhí)行完,由于某種原因(如,I/O操作等)讓出CPU執(zhí)行權(quán),自身進(jìn)入阻塞狀態(tài)。
Dead:死亡狀態(tài),線(xiàn)程執(zhí)行完成或者執(zhí)行過(guò)程中出現(xiàn)異常,線(xiàn)程就會(huì)進(jìn)入死亡狀態(tài)。
這五種狀態(tài)之間的轉(zhuǎn)換關(guān)系如下圖所示:
4.2、wait/notify/notifyAll方法的使用
【1】wait方法
有三種版本:
(1)wait(),作用是將當(dāng)前運(yùn)行的線(xiàn)程掛起(即讓其進(jìn)入阻塞狀態(tài)),直到notify或notifyAll方法來(lái)喚醒線(xiàn)程;
(2)wait(long timeout),該方法與wait()方法類(lèi)似,唯一的區(qū)別就是在指定時(shí)間內(nèi),如果沒(méi)有notify或notifAll方法的喚醒,也會(huì)自動(dòng)喚醒;
(3)wait(long timeout,long nanos),在于更精確的控制調(diào)度時(shí)間;
核心:wait方法的使用必須在同步的范圍內(nèi),否則就會(huì)拋出IllegalMonitorStateException異常,wait方法的作用就是阻塞當(dāng)前線(xiàn)程等待notify/notifyAll方法的喚醒,或等待超時(shí)后自動(dòng)喚醒。
public class WaitTest {public void testWait(){System.out.println("Start-----");try {wait(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("End-------");}public static void main(String[] args) {final WaitTest test = new WaitTest();new Thread(new Runnable() {@Overridepublic void run() {test.testWait();}}).start();} }如果按照以上代碼這么寫(xiě),會(huì)拋出IllegalMonitorStateException異常,意思是:線(xiàn)程試圖等待對(duì)象的監(jiān)視器或者試圖通知其他正在等待對(duì)象監(jiān)視器的線(xiàn)程,但本身沒(méi)有對(duì)應(yīng)的監(jiān)視器的所有權(quán)。
由于wait方法是一個(gè)本地方法,其底層是通過(guò)一個(gè)叫做監(jiān)視器鎖的對(duì)象monitor來(lái)完成的。所以上面之所以會(huì)拋出異常,是因?yàn)樵谡{(diào)用wait方式時(shí)沒(méi)有獲取到monitor對(duì)象的所有權(quán),那如何獲取monitor對(duì)象所有權(quán)?Java中只能通過(guò)Synchronized關(guān)鍵字來(lái)完成,修改上述代碼,增加Synchronized關(guān)鍵字,即可運(yùn)行:
public class WaitTest {public synchronized void testWait(){//增加Synchronized關(guān)鍵字System.out.println("Start-----");try {wait(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("End-------");}public static void main(String[] args) {final WaitTest test = new WaitTest();new Thread(new Runnable() {@Overridepublic void run() {test.testWait();}}).start();} }通過(guò)上述代碼示例,可以看出,wait方法的使用必須在同步的范圍內(nèi),否則就會(huì)拋出IllegalMonitorStateException異常,wait方法的作用就是阻塞當(dāng)前線(xiàn)程等待notify/notifyAll方法的喚醒,或等待超時(shí)后自動(dòng)喚醒。
【2】notify/notifyall
有了對(duì)wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通過(guò)對(duì)象的monitor對(duì)象來(lái)實(shí)現(xiàn)的,所以只要在同一對(duì)象上去調(diào)用notify/notifyAll方法,就可以喚醒對(duì)應(yīng)對(duì)象monitor上等待的線(xiàn)程了。notify和notifyAll的區(qū)別在于前者只能喚醒monitor上的一個(gè)線(xiàn)程,對(duì)其他線(xiàn)程沒(méi)有影響,而notifyAll則喚醒所有的線(xiàn)程。
有兩點(diǎn)點(diǎn)需要注意:
(1)調(diào)用wait方法后,線(xiàn)程是會(huì)釋放對(duì)monitor對(duì)象的所有權(quán)的。
(2)一個(gè)通過(guò)wait方法阻塞的線(xiàn)程,必須同時(shí)滿(mǎn)足以下兩個(gè)條件才能被真正執(zhí)行:
線(xiàn)程需要被喚醒(超時(shí)喚醒或調(diào)用notify/notifyll)。
線(xiàn)程喚醒后需要競(jìng)爭(zhēng)到鎖(monitor)。
4.3、sleep/yield/join方法使用:
再來(lái)看另外一組線(xiàn)程間協(xié)作的方法。這組方法跟上面方法的最明顯區(qū)別是:這幾個(gè)方法都位于Thread類(lèi)中,而上面三個(gè)方法都位于Object類(lèi)中。
【1】sleep方法
sleep方法的作用是讓當(dāng)前線(xiàn)程暫停指定的時(shí)間(毫秒),sleep方法是最簡(jiǎn)單的方法,比較容易理解。
唯一需要注意的是其與wait方法的區(qū)別。最簡(jiǎn)單的區(qū)別是,wait方法依賴(lài)于同步,而sleep方法可以直接調(diào)用。而更深層次的區(qū)別在于sleep方法只是暫時(shí)讓出CPU的執(zhí)行權(quán),并不釋放鎖。而wait方法則需要釋放鎖。
通過(guò)sleep方法實(shí)現(xiàn)的暫停,程序是順序進(jìn)入同步塊的,只有當(dāng)上一個(gè)線(xiàn)程執(zhí)行完成的時(shí)候,下一個(gè)線(xiàn)程才能進(jìn)入同步方法,sleep暫停期間一直持有monitor對(duì)象鎖,其他線(xiàn)程是不能進(jìn)入的。而wait方法則不同,當(dāng)調(diào)用wait方法后,當(dāng)前線(xiàn)程會(huì)釋放持有的monitor對(duì)象鎖,因此,其他線(xiàn)程還可以進(jìn)入到同步方法,線(xiàn)程被喚醒后,需要競(jìng)爭(zhēng)鎖,獲取到鎖之后再繼續(xù)執(zhí)行。
【2】yield方法
yield方法的作用是暫停當(dāng)前線(xiàn)程,以便其他線(xiàn)程有機(jī)會(huì)執(zhí)行,不過(guò)不能指定暫停的時(shí)間,并且也不能保證當(dāng)前線(xiàn)程馬上停止。yield方法只是將Running狀態(tài)轉(zhuǎn)變?yōu)镽unnable狀態(tài)。
不過(guò)請(qǐng)注意:這種交替并不一定能得到保證,而且源碼中也對(duì)這個(gè)問(wèn)題進(jìn)行了說(shuō)明,意思總結(jié)是:調(diào)度器可能會(huì)忽略該方法;使用的時(shí)候要仔細(xì)分析和測(cè)試,確保能達(dá)到預(yù)期的效果;很少有場(chǎng)景要用到該方法,主要使用的地方是調(diào)試和測(cè)試。
【3】join方法
有三個(gè)版本:
void join()?? ?
void join(long millis)
void join(long millis, int nanos)
join方法的作用是:父線(xiàn)程等待子線(xiàn)程執(zhí)行完成后再執(zhí)行,換句話(huà)說(shuō)就是將異步執(zhí)行的線(xiàn)程合并為同步的線(xiàn)程。JDK中提供三個(gè)版本的join方法,其實(shí)現(xiàn)與wait方法類(lèi)似,join()方法實(shí)際上執(zhí)行的join(0),而join(long millis, int nanos)也與wait(long millis, int nanos)的實(shí)現(xiàn)方式一致,暫時(shí)對(duì)納秒的支持也是不完整的。
重點(diǎn)關(guān)注一下join(long millis)方法的實(shí)現(xiàn),可以看出join方法就是通過(guò)wait方法來(lái)將線(xiàn)程的阻塞,如果join的線(xiàn)程還在執(zhí)行,則將當(dāng)前線(xiàn)程阻塞起來(lái),直到j(luò)oin的線(xiàn)程執(zhí)行完成,當(dāng)前線(xiàn)程才能執(zhí)行。不過(guò)有一點(diǎn)需要注意,這里的join只調(diào)用了wait方法,卻沒(méi)有對(duì)應(yīng)的notify方法,原因是Thread的start方法中做了相應(yīng)的處理,所以當(dāng)join的線(xiàn)程執(zhí)行完成以后,會(huì)自動(dòng)喚醒主線(xiàn)程繼續(xù)往下執(zhí)行。
在沒(méi)有使用join方法之間,線(xiàn)程是并發(fā)執(zhí)行的,而使用join方法后,所有線(xiàn)程是順序執(zhí)行的。
總結(jié):
以上這部分詳細(xì)講解了wait/notify/notifyAll和sleep/yield/join方法。
最后回答一個(gè)問(wèn)題:wait/notify/notifyAll方法的作用是實(shí)現(xiàn)線(xiàn)程間的協(xié)作,那為什么這三個(gè)方法不是位于Thread類(lèi)中,而是位于Object類(lèi)中?
位于Object中,也就相當(dāng)于所有類(lèi)都包含這三個(gè)方法(因?yàn)镴ava中所有的類(lèi)都繼承自O(shè)bject類(lèi))。要回答這個(gè)問(wèn)題,還是得回過(guò)來(lái)看wait方法的實(shí)現(xiàn)原理,大家需要明白的是,wait等待的到底是什么東西?如果對(duì)上面內(nèi)容理解的比較好的話(huà),我相信大家應(yīng)該很容易知道wait等待其實(shí)是對(duì)象monitor,由于Java中的每一個(gè)對(duì)象都有一個(gè)內(nèi)置的monitor對(duì)象,自然所有的類(lèi)都理應(yīng)有wait/notify方法。
4.4、monitor詳解:
Java平臺(tái)中,每個(gè)對(duì)象都有一個(gè)唯一與之對(duì)應(yīng)的內(nèi)部鎖(Monitor)。Java虛擬機(jī)會(huì)為每個(gè)對(duì)象維護(hù)兩個(gè)“隊(duì)列”(姑且稱(chēng)之為“隊(duì)列”,盡管它不一定符合數(shù)據(jù)結(jié)構(gòu)上隊(duì)列的“先進(jìn)先出”原則):一個(gè)叫Entry Set(入口集),另外一個(gè)叫Wait Set(等待集)。對(duì)于任意的對(duì)象objectX,objectX的Entry Set用于存儲(chǔ)等待獲取objectX對(duì)應(yīng)的內(nèi)部鎖的所有線(xiàn)程。objectX的Wait Set用于存儲(chǔ)執(zhí)行了objectX.wait()/wait(long)的線(xiàn)程。
?
設(shè)objectX是任意一個(gè)對(duì)象,monitorX是這個(gè)對(duì)象對(duì)應(yīng)的內(nèi)部鎖,假設(shè)有線(xiàn)程A、B、C同時(shí)申請(qǐng)monitorX,那么由于任意一個(gè)時(shí)刻只有一個(gè)線(xiàn)程能夠獲得(占用/持有)這個(gè)鎖,因此除了勝出(即獲得了鎖)的線(xiàn)程(這里假設(shè)是B)外,其他線(xiàn)程(這里就是A和C)都會(huì)被暫停(線(xiàn)程的生命周期狀態(tài)會(huì)被調(diào)整為BLOCKED)。這些因申請(qǐng)鎖而落選的線(xiàn)程就會(huì)被存入objectX對(duì)應(yīng)的Entry Set(以下記為entrySetX)之中。當(dāng)monitorX被其持有線(xiàn)程(這里就是B)釋放時(shí),entrySetX中的一個(gè)任意(注意是“任意”,而不一定是Entry Set中等待時(shí)間最長(zhǎng)或者最短的)線(xiàn)程會(huì)被喚醒(即線(xiàn)程的生命周期狀態(tài)變更為RUNNABLE)。這個(gè)被喚醒的線(xiàn)程會(huì)與其他活躍線(xiàn)程(即不處于Entry Set之中,且線(xiàn)程的生命周期狀態(tài)為RUNNABLE的線(xiàn)程)再次搶占monitorX。這時(shí),被喚醒的線(xiàn)程如果成功申請(qǐng)到monitorX,那么該線(xiàn)程就從entrySetX中移除。否則,被喚醒的線(xiàn)程仍然會(huì)停留在entrySetX,并再次被暫停,以等待下次申請(qǐng)鎖的機(jī)會(huì)。
?
如果有個(gè)線(xiàn)程執(zhí)行了objectX.wait(),那么該線(xiàn)程就會(huì)被暫停(線(xiàn)程的生命周期狀態(tài)會(huì)被調(diào)整為WAITTING)并被存入objectX的Wait Set(以下記為waitSetX)之中。此時(shí),該線(xiàn)程就被稱(chēng)為objectX的等待線(xiàn)程。當(dāng)其他線(xiàn)程執(zhí)行了objectX.notify()/notifyAll()時(shí),waitSetX中的一個(gè)(或者多個(gè),取決于被調(diào)用的是notify還是notifyAll方法)任意(注意是“任意”,而不一定是Entry Set中等待時(shí)間最長(zhǎng)或者最短的)等待線(xiàn)程會(huì)被喚醒(線(xiàn)程的生命周期狀態(tài)變更為RUNNABLE)。這些被喚醒的線(xiàn)程會(huì)與entrySetX中被喚醒的線(xiàn)程以及其他(可能的)活躍線(xiàn)程共同參與搶奪monitorX。如果其中一個(gè)被喚醒的等待線(xiàn)程成功申請(qǐng)到鎖,那么該線(xiàn)程就會(huì)從waitSetX中移除。否則,這些被喚醒的線(xiàn)程仍然停留在waitSetX中,并再次被暫停,以等待下次申請(qǐng)鎖的機(jī)會(huì)。
問(wèn)題:我理解調(diào)用對(duì)象的 notifyAll方法后,waitSet 上的線(xiàn)程都會(huì)加入到 entrySet 中的吧?在一個(gè)持有鎖的線(xiàn)程釋放鎖后,應(yīng)該只有 entrySet 隊(duì)列的線(xiàn)程可能獲取鎖,那這個(gè)通知是 park 來(lái)實(shí)現(xiàn)的嗎?是否有保證獲取鎖公平性的相關(guān)設(shè)置?
【1】從Java虛擬機(jī)性能的角度來(lái)說(shuō),Java虛擬機(jī)沒(méi)有必要在notifyAll調(diào)用之后“將Wait Set中的線(xiàn)程移入Entry Set”。首先,從一個(gè)“隊(duì)列”移動(dòng)到另外一個(gè)“隊(duì)列”是有開(kāi)銷(xiāo)的,其次,雖然notifyAll調(diào)用后Wait Set中的多個(gè)線(xiàn)程會(huì)被喚醒,但是這些被喚醒的線(xiàn)程極端情況下可能沒(méi)有任何一個(gè)能夠獲得鎖(比如被其他活躍線(xiàn)程搶先下手了)或者即便可以獲得鎖也可能不能繼續(xù)運(yùn)行(比如這些等待線(xiàn)程所需的等待條件又再次不成立)。那么這個(gè)時(shí)候,這些等待線(xiàn)程仍然需要老老實(shí)實(shí)在wait set中待著。因此,如果notifyAll調(diào)用之后就將等待線(xiàn)程移出wait set會(huì)導(dǎo)致浪費(fèi)(白白地進(jìn)出“隊(duì)列”)。這點(diǎn)可以參考顯式鎖的實(shí)現(xiàn):
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(Node, int)/*** Acquires in exclusive uninterruptible mode for thread already in* queue. Used by condition wait methods as well as acquire.** @param node the node* @param arg the acquire argument* @return {@code true} if interrupted while waiting*/ final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);} }從上面的代碼可以看出,(使用顯式鎖時(shí))被喚醒的線(xiàn)程獲得鎖(tryAcquire調(diào)用返回true)之后才被從wait set中移出(setHead調(diào)用)。
【2】?jī)?nèi)部鎖僅僅支持非公平鎖調(diào)度。顯式鎖既支持公平鎖又支持非公平鎖。
LockSupport.park/upark是在jdk1.5開(kāi)始引入的,顯式鎖的在實(shí)現(xiàn)線(xiàn)程的暫停和喚醒的時(shí)候會(huì)用到這個(gè)兩個(gè)方法。而內(nèi)部鎖是在jdk1.5之前就已經(jīng)存在的。
?
?
參考:
Java 并發(fā)編程:線(xiàn)程間的協(xié)作(wait/notify/sleep/yield/join):https://www.cnblogs.com/paddix/p/5381958.html
java的鎖池和等待池:https://www.cnblogs.com/tiancai/p/9371655.html
總結(jié)
以上是生活随笔為你收集整理的java monitor的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: graphics库的使用
- 下一篇: 无法启动程序,.dll不是有效的Win3