線程間的協(xié)作
線程之間相互配合瑟枫,完成某項(xiàng)工作哩俭,比如:一個(gè)線程修改了一個(gè)對(duì)象的值拥诡, 而另一個(gè)線程感知到了變化触趴,然后進(jìn)行相應(yīng)的操作,整個(gè)過程開始于一個(gè)線程渴肉, 而最終執(zhí)行又是另一個(gè)線程冗懦。前者是生產(chǎn)者,后者就是消費(fèi)者仇祭,這種模式隔離了 “做什么”(what)和“怎么做”(How)披蕉,簡單的辦法是讓消費(fèi)者線程不斷地循環(huán)檢查變量是否符合預(yù)期在 while 循環(huán)中設(shè)置不滿足的條件,如果條件滿足則退出 while 循環(huán)乌奇,從而完成消費(fèi)者的工作没讲。卻存在如下問題:
1) 難以確保及時(shí)性。
2)難以降低開銷礁苗。如果降低睡眠的時(shí)間爬凑,比如休眠 1 毫秒,這樣消費(fèi)者能 更加迅速地發(fā)現(xiàn)條件變化试伙,但是卻可能消耗更多的處理器資源嘁信,造成了無端的浪 費(fèi)。
等待/通知機(jī)制
是指一個(gè)線程 A 調(diào)用了對(duì)象 O 的 wait()方法進(jìn)入等待狀態(tài)疏叨,而另一個(gè)線程 B 調(diào)用了對(duì)象 O 的 notify()或者 notifyAll()方法潘靖,線程 A 收到通知后從對(duì)象 O 的 wait() 方法返回,進(jìn)而執(zhí)行后續(xù)操作蚤蔓。上述兩個(gè)線程通過對(duì)象 O 來完成交互卦溢,而對(duì)象 上的 wait()和 notify/notifyAll()的關(guān)系就如同開關(guān)信號(hào)一樣,用來完成等待方和通 知方之間的交互工作秀又。
- notify()
通知一個(gè)在對(duì)象上等待的線程,使其從 wait 方法返回,而返回的前提是該線程 獲取到了對(duì)象的鎖单寂,沒有獲得鎖的線程重新進(jìn)入 WAITING 狀態(tài)。 - notifyAll()
通知所有等待在該對(duì)象上的線程 - wait()
調(diào)用該方法的線程進(jìn)入 WAITING 狀態(tài),只有等待另外線程的通知或被中斷才會(huì)返回吐辙。需要注意凄贩,調(diào)用 wait()方法后,會(huì)釋放對(duì)象的鎖 - wait(long)
超時(shí)等待一段時(shí)間,這里的參數(shù)時(shí)間是毫秒,也就是等待長達(dá) n 毫秒,如果沒有通知就超時(shí)返回 - wait (long,int)
對(duì)于超時(shí)時(shí)間更細(xì)粒度的控制袱讹,可以達(dá)到納秒
等待和通知的標(biāo)準(zhǔn)范式
等待方遵循如下原則疲扎。
1)獲取對(duì)象的鎖昵时。
2)如果條件不滿足,那么調(diào)用對(duì)象的 wait() 方法椒丧,被通知后仍要檢查條件壹甥。
3)條件滿足則執(zhí)行對(duì)應(yīng)的邏輯。
通知方遵循如下原則壶熏。
1)獲得對(duì)象的鎖句柠。
2)改變條件。
3)通知所有等待在對(duì)象上的線程棒假。
在調(diào)用 wait()溯职、notify() 系列方法之前,線程必須要獲得該對(duì)象的對(duì)象級(jí)別鎖帽哑,即只能在同步方法或同步塊中調(diào)用 wait() 方法谜酒、notify()系列方法,進(jìn)入 wait() 方法后妻枕,當(dāng)前線程釋放鎖僻族,在從 wait() 返回前,線程與其他線程競(jìng)爭重新獲得鎖屡谐,執(zhí)行 notify() 系列方法的線程退出調(diào)用了 notifyAll 的 synchronized 代碼塊的時(shí)候后述么,他們就會(huì)去競(jìng)爭。如果其中一個(gè)線程獲得了該對(duì)象鎖愕掏,它就會(huì)繼續(xù)往下執(zhí)行度秘,在它退出 synchronized 代碼塊,釋放鎖后饵撑,其他的已經(jīng)被喚醒的線程將會(huì)繼續(xù)競(jìng)爭獲取該鎖敷钾,一直進(jìn)行下去,直到所有被喚醒的線程都執(zhí)行完畢肄梨。
notify 和 notifyAll 應(yīng)該用誰
盡可能用 notifyall(),謹(jǐn)慎使用 notify()挠锥,因?yàn)?notify() 只會(huì)喚醒一個(gè)線程众羡,我們無法確保被喚醒的這個(gè)線程一定就是我們需要喚醒的線程
wait/notify/notifyAll 方法的使用注意事項(xiàng)。
我們主要從三個(gè)問題入手:
1. 為什么 wait 方法必須在 synchronized 保護(hù)的同步代碼中使用蓖租?
2. 為什么 wait/notify/notifyAll 被定義在 Object 類中粱侣,而 sleep 定義在 Thread 類中?
3. wait/notify 和 sleep 方法的異同蓖宦?
為什么 wait 方法必須在 synchronized 保護(hù)的同步代碼中使用齐婴?
先來看看 wait 方法的源碼注釋是怎么寫的。
“wait method should always be used in a loop:
synchronized (obj) {
while (condition does not hold)
obj.wait();
... // Perform action appropriate to condition
}
This method should only be called by a thread that is the owner of this object's monitor.”
意思是說稠茂,在使用 wait 方法時(shí)柠偶,必須把 wait 方法寫在 synchronized 保護(hù)的 while 代碼塊中情妖,并始終判斷執(zhí)行條件是否滿足,如果滿足就往下繼續(xù)執(zhí)行诱担,如果不滿足就執(zhí)行 wait 方法毡证,而在執(zhí)行 wait 方法之前,必須先持有對(duì)象的 monitor 鎖蔫仙,也就是通常所說的 synchronized 鎖料睛。
如果不要求 wait 方法放在 synchronized 保護(hù)的同步代碼中使用,而是可以隨意調(diào)用摇邦,實(shí)例代碼如下:
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data) {
buffer.add(data);
notify(); // Since someone may be waiting in take
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
give 方法負(fù)責(zé)往 buffer 中添加數(shù)據(jù)恤煞,添加完之后執(zhí)行 notify
方法來喚醒之前等待的線程,而 take 方法負(fù)責(zé)檢查整個(gè) buffer 是否為空施籍,如果為空就進(jìn)入等待居扒,如果不為空就取出一個(gè)數(shù)據(jù),這是典型的生產(chǎn)者消費(fèi)者的思想法梯。
這段代碼并沒有受 synchronized 保護(hù)苔货,于是便有可能發(fā)生以下場(chǎng)景:
首先,消費(fèi)者線程調(diào)用 take 方法并判斷 buffer.isEmpty 方法是否返回 true立哑,若為 true 代表 buffer 是空的夜惭,則線程希望進(jìn)入等待,但是在線程調(diào)用 wait 方法之前铛绰,就被調(diào)度器暫停了诈茧,所以此時(shí)還沒來得及執(zhí)行 wait 方法。
此時(shí)生產(chǎn)者開始運(yùn)行捂掰,執(zhí)行了整個(gè) give 方法敢会,它往 buffer 中添加了數(shù)據(jù),并執(zhí)行了 notify 方法这嚣,但 notify 并沒有任何效果鸥昏,因?yàn)橄M(fèi)者線程的 wait 方法沒來得及執(zhí)行,所以沒有線程在等待被喚醒姐帚。
此時(shí)吏垮,剛才被調(diào)度器暫停的消費(fèi)者線程回來繼續(xù)執(zhí)行 wait 方法并進(jìn)入了等待。
雖然剛才消費(fèi)者判斷了 buffer.isEmpty 條件罐旗,但真正執(zhí)行 wait 方法時(shí)膳汪,之前的 buffer.isEmpty 的結(jié)果已經(jīng)過期了,不再符合最新的場(chǎng)景了九秀,因?yàn)檫@里的“判斷-執(zhí)行”不是一個(gè)原子操作遗嗽,它在中間被打斷了,是線程不安全的
假設(shè)這時(shí)沒有更多的生產(chǎn)者進(jìn)行生產(chǎn)鼓蜒,消費(fèi)者便有可能陷入無窮無盡的等待痹换,因?yàn)樗e(cuò)過了剛才 give 方法內(nèi)的 notify 的喚醒征字。
因?yàn)?wait 方法所在的 take 方法沒有被 synchronized 保護(hù),所以它的 while 判斷和 wait 方法無法構(gòu)成原子操作晴音,那么此時(shí)整個(gè)程序就很容易出錯(cuò)柔纵。
把代碼改寫成源碼注釋所要求的被 synchronized
保護(hù)的同步代碼塊的形式,代碼如下锤躁。
public void give(String data) {
synchronized (this) {
buffer.add(data);
notify();
}
}
public String take() throws InterruptedException {
synchronized (this) {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
這樣就可以確保 notify 方法永遠(yuǎn)不會(huì)在 buffer.isEmpty 和 wait 方法之間被調(diào)用搁料,提升了程序的安全性。
wait 方法會(huì)釋放 monitor 鎖系羞,這也要求我們必須首先進(jìn)入到 synchronized
內(nèi)持有這把鎖郭计。
這里還存在一個(gè)“虛假喚醒”(spurious wakeup)的問題,線程可能在既沒有被notify/notifyAll椒振,也沒有被中斷或者超時(shí)的情況下被喚醒昭伸,這種喚醒是我們不希望看到的。雖然在實(shí)際生產(chǎn)中澎迎,虛假喚醒發(fā)生的概率很小庐杨,但是程序依然需要保證在發(fā)生虛假喚醒的時(shí)候的正確性,所以就需要采用while循環(huán)的結(jié)構(gòu)夹供。
while (condition does not hold)
obj.wait();
這樣即便被虛假喚醒了灵份,也會(huì)再次檢查while里面的條件,如果不滿足條件哮洽,就會(huì)繼續(xù)wait填渠,也就消除了虛假喚醒的風(fēng)險(xiǎn)。
為什么 wait/notify/notifyAll 被定義在 Object 類中鸟辅,而 sleep 定義在 Thread 類中氛什?
主要有兩點(diǎn)原因:
1. 因?yàn)?Java 中每個(gè)對(duì)象都有一把稱之為 monitor 監(jiān)視器的鎖,由于每個(gè)對(duì)象都可以上鎖匪凉,這就要求在對(duì)象頭中有一個(gè)用來保存鎖信息的位置枪眉。這個(gè)鎖是對(duì)象級(jí)別的,而非線程級(jí)別的再层,wait/notify/notifyAll 也都是鎖級(jí)別的操作贸铜,它們的鎖屬于對(duì)象,所以把它們定義在 Object 類中是最合適树绩,因?yàn)?Object 類是所有對(duì)象的父類。
2. 因?yàn)槿绻?wait/notify/notifyAll 方法定義在 Thread 類中隐轩,會(huì)帶來很大的局限性饺饭,比如一個(gè)線程可能持有多把鎖,以便實(shí)現(xiàn)相互配合的復(fù)雜邏輯职车,假設(shè)此時(shí) wait 方法定義在 Thread 類中瘫俊,如何實(shí)現(xiàn)讓一個(gè)線程持有多把鎖呢鹊杖?又如何明確線程等待的是哪把鎖呢?既然我們是讓當(dāng)前線程去等待某個(gè)對(duì)象的鎖扛芽,自然應(yīng)該通過操作對(duì)象來實(shí)現(xiàn)骂蓖,而不是操作線程。
wait/notify 和 sleep 方法的異同川尖?
相同點(diǎn):
1. 它們都可以讓線程阻塞登下。
2. 它們都可以響應(yīng) interrupt 中斷:在等待的過程中如果收到中斷信號(hào),都可以進(jìn)行響應(yīng)叮喳,并拋出 InterruptedException 異常被芳。
不同點(diǎn):
- wait 方法必須在 synchronized 保護(hù)的代碼中使用,而 sleep 方法并沒有這個(gè)要求馍悟。
- 在同步代碼中執(zhí)行 sleep 方法時(shí)畔濒,并不會(huì)釋放 monitor 鎖,但執(zhí)行 wait 方法時(shí)會(huì)主動(dòng)釋放 monitor 鎖锣咒。
- sleep 方法中會(huì)要求必須定義一個(gè)時(shí)間侵状,時(shí)間到期后會(huì)主動(dòng)恢復(fù),而對(duì)于沒有參數(shù)的 wait 方法而言毅整,意味著永久等待趣兄,直到被中斷或被喚醒才能恢復(fù),它并不會(huì)主動(dòng)恢復(fù)毛嫉。
- wait/notify 是 Object 類的方法诽俯,而 sleep 是 Thread 類的方法。