java 多線程-線程通信實(shí)例講解
線程通信的目標(biāo)是使線程間能夠互相發(fā)送信號(hào)。另一方面,線程通信使線程能夠等待其他線程的信號(hào)。
- 通過(guò)共享對(duì)象通信
- 忙等待
- wait(),notify()和 notifyAll()
- 丟失的信號(hào)
- 假喚醒
- 多線程等待相同信號(hào)
- 不要對(duì)常量字符串或全局對(duì)象調(diào)用 wait()
通過(guò)共享對(duì)象通信
線程間發(fā)送信號(hào)的一個(gè)簡(jiǎn)單方式是在共享對(duì)象的變量里設(shè)置信號(hào)值。線程 A 在一個(gè)同步塊里設(shè)置 boolean 型成員變量 hasDataToProcess 為 true,線程 B 也在同步塊里讀取 hasDataToProcess 這個(gè)成員變量。這個(gè)簡(jiǎn)單的例子使用了一個(gè)持有信號(hào)的對(duì)象,并提供了 set 和 check 方法:
public class MySignal{ protected boolean hasDataToProcess = false; public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; } public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess = hasData; } }
線程 A 和 B 必須獲得指向一個(gè) MySignal 共享實(shí)例的引用,以便進(jìn)行通信。如果它們持有的引用指向不同的 MySingal 實(shí)例,那么彼此將不能檢測(cè)到對(duì)方的信號(hào)。需要處理的數(shù)據(jù)可以存放在一個(gè)共享緩存區(qū)里,它和 MySignal 實(shí)例是分開存放的。
忙等待(Busy Wait)
準(zhǔn)備處理數(shù)據(jù)的線程 B 正在等待數(shù)據(jù)變?yōu)榭捎?。換句話說(shuō),它在等待線程 A 的一個(gè)信號(hào),這個(gè)信號(hào)使 hasDataToProcess()返回 true。線程 B 運(yùn)行在一個(gè)循環(huán)里,以等待這個(gè)信號(hào):
protected MySignal sharedSignal = ... ... while(!sharedSignal.hasDataToProcess()){ //do nothing... busy waiting }
wait(),notify()和 notifyAll()
忙等待沒(méi)有對(duì)運(yùn)行等待線程的 CPU 進(jìn)行有效的利用,除非平均等待時(shí)間非常短。否則,讓等待線程進(jìn)入睡眠或者非運(yùn)行狀態(tài)更為明智,直到它接收到它等待的信號(hào)。
Java 有一個(gè)內(nèi)建的等待機(jī)制來(lái)允許線程在等待信號(hào)的時(shí)候變?yōu)榉沁\(yùn)行狀態(tài)。java.lang.Object 類定義了三個(gè)方法,wait()、notify()和 notifyAll()來(lái)實(shí)現(xiàn)這個(gè)等待機(jī)制。
一個(gè)線程一旦調(diào)用了任意對(duì)象的 wait()方法,就會(huì)變?yōu)榉沁\(yùn)行狀態(tài),直到另一個(gè)線程調(diào)用了同一個(gè)對(duì)象的 notify()方法。為了調(diào)用 wait()或者 notify(),線程必須先獲得那個(gè)對(duì)象的鎖。也就是說(shuō),線程必須在同步塊里調(diào)用 wait()或者 notify()。以下是 MySingal 的修改版本——使用了 wait()和 notify()的 MyWaitNotify:
public class MonitorObject{ } public class MyWaitNotify{ MonitorObject myMonitorObject = new MonitorObject(); public void doWait(){ synchronized(myMonitorObject){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } } public void doNotify(){ synchronized(myMonitorObject){ myMonitorObject.notify(); } } }
等待線程將調(diào)用 doWait(),而喚醒線程將調(diào)用 doNotify()。當(dāng)一個(gè)線程調(diào)用一個(gè)對(duì)象的 notify()方法,正在等待該對(duì)象的所有線程中將有一個(gè)線程被喚醒并允許執(zhí)行(校注:這個(gè)將被喚醒的線程是隨機(jī)的,不可以指定喚醒哪個(gè)線程)。同時(shí)也提供了一個(gè) notifyAll()方法來(lái)喚醒正在等待一個(gè)給定對(duì)象的所有線程。
如你所見(jiàn),不管是等待線程還是喚醒線程都在同步塊里調(diào)用 wait()和 notify()。這是強(qiáng)制性的!一個(gè)線程如果沒(méi)有持有對(duì)象鎖,將不能調(diào)用 wait(),notify()或者 notifyAll()。否則,會(huì)拋出 IllegalMonitorStateException 異常。
(校注:JVM 是這么實(shí)現(xiàn)的,當(dāng)你調(diào)用 wait 時(shí)候它首先要檢查下當(dāng)前線程是否是鎖的擁有者,不是則拋出 IllegalMonitorStateExcept。)
但是,這怎么可能?等待線程在同步塊里面執(zhí)行的時(shí)候,不是一直持有監(jiān)視器對(duì)象(myMonitor 對(duì)象)的鎖嗎?等待線程不能阻塞喚醒線程進(jìn)入 doNotify()的同步塊嗎?答案是:的確不能。一旦線程調(diào)用了 wait()方法,它就釋放了所持有的監(jiān)視器對(duì)象上的鎖。這將允許其他線程也可以調(diào)用 wait()或者 notify()。
一旦一個(gè)線程被喚醒,不能立刻就退出 wait()的方法調(diào)用,直到調(diào)用 notify()的
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
線程退出了它自己的同步塊。換句話說(shuō):被喚醒的線程必須重新獲得監(jiān)視器對(duì)象的鎖,才可以退出 wait()的方法調(diào)用,因?yàn)?wait 方法調(diào)用運(yùn)行在同步塊里面。如果多個(gè)線程被 notifyAll()喚醒,那么在同一時(shí)刻將只有一個(gè)線程可以退出 wait()方法,因?yàn)槊總€(gè)線程在退出 wait()前必須獲得監(jiān)視器對(duì)象的鎖。
丟失的信號(hào)(Missed Signals)
notify()和 notifyAll()方法不會(huì)保存調(diào)用它們的方法,因?yàn)楫?dāng)這兩個(gè)方法被調(diào)用時(shí),有可能沒(méi)有線程處于等待狀態(tài)。通知信號(hào)過(guò)后便丟棄了。因此,如果一個(gè)線程先于被通知線程調(diào)用 wait()前調(diào)用了 notify(),等待的線程將錯(cuò)過(guò)這個(gè)信號(hào)。這可能是也可能不是個(gè)問(wèn)題。不過(guò),在某些情況下,這可能使等待線程永遠(yuǎn)在等待,不再醒來(lái),因?yàn)榫€程錯(cuò)過(guò)了喚醒信號(hào)。
為了避免丟失信號(hào),必須把它們保存在信號(hào)類里。在 MyWaitNotify 的例子中,通知信號(hào)應(yīng)被存儲(chǔ)在 MyWaitNotify 實(shí)例的一個(gè)成員變量里。以下是 MyWaitNotify 的修改版本:
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
留意 doNotify()方法在調(diào)用 notify()前把 wasSignalled 變量設(shè)為 true。同時(shí),留意 doWait()方法在調(diào)用 wait()前會(huì)檢查 wasSignalled 變量。事實(shí)上,如果沒(méi)有信號(hào)在前一次 doWait()調(diào)用和這次 doWait()調(diào)用之間的時(shí)間段里被接收到,它將只調(diào)用 wait()。
(校注:為了避免信號(hào)丟失, 用一個(gè)變量來(lái)保存是否被通知過(guò)。在 notify 前,設(shè)置自己已經(jīng)被通知過(guò)。在 wait 后,設(shè)置自己沒(méi)有被通知過(guò),需要等待通知。)
假喚醒
由于莫名其妙的原因,線程有可能在沒(méi)有調(diào)用過(guò) notify()和 notifyAll()的情況下醒來(lái)。這就是所謂的假喚醒(spurious wakeups)。無(wú)端端地醒過(guò)來(lái)了。
如果在 MyWaitNotify2 的 doWait()方法里發(fā)生了假喚醒,等待線程即使沒(méi)有收到正確的信號(hào),也能夠執(zhí)行后續(xù)的操作。這可能導(dǎo)致你的應(yīng)用程序出現(xiàn)嚴(yán)重問(wèn)題。
為了防止假喚醒,保存信號(hào)的成員變量將在一個(gè) while 循環(huán)里接受檢查,而不是在 if 表達(dá)式里。這樣的一個(gè) while 循環(huán)叫做自旋鎖(校注:這種做法要慎重,目前的 JVM 實(shí)現(xiàn)自旋會(huì)消耗 CPU,如果長(zhǎng)時(shí)間不調(diào)用 doNotify 方法,doWait 方法會(huì)一直自旋,CPU 會(huì)消耗太大)。被喚醒的線程會(huì)自旋直到自旋鎖(while 循環(huán))里的條件變?yōu)?false。以下 MyWaitNotify2 的修改版本展示了這點(diǎn):
public class MyWaitNotify3{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
留意 wait()方法是在 while 循環(huán)里,而不在 if 表達(dá)式里。如果等待線程沒(méi)有收到信號(hào)就喚醒,wasSignalled 變量將變?yōu)?false,while 循環(huán)會(huì)再執(zhí)行一次,促使醒來(lái)的線程回到等待狀態(tài)。
多個(gè)線程等待相同信號(hào)
如果你有多個(gè)線程在等待,被 notifyAll()喚醒,但只有一個(gè)被允許繼續(xù)執(zhí)行,使用 while 循環(huán)也是個(gè)好方法。每次只有一個(gè)線程可以獲得監(jiān)視器對(duì)象鎖,意味著只有一個(gè)線程可以退出 wait()調(diào)用并清除 wasSignalled 標(biāo)志(設(shè)為 false)。一旦這個(gè)線程退出 doWait()的同步塊,其他線程退出 wait()調(diào)用,并在 while 循環(huán)里檢查 wasSignalled 變量值。但是,這個(gè)標(biāo)志已經(jīng)被第一個(gè)喚醒的線程清除了,所以其余醒來(lái)的線程將回到等待狀態(tài),直到下次信號(hào)到來(lái)。
不要在字符串常量或全局對(duì)象中調(diào)用 wait()
(校注:本章說(shuō)的字符串常量指的是值為常量的變量)
本文早期的一個(gè)版本在 MyWaitNotify 例子里使用字符串常量(””)作為管程對(duì)象。以下是那個(gè)例子:
public class MyWaitNotify{ String myMonitorObject = ""; boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
在空字符串作為鎖的同步塊(或者其他常量字符串)里調(diào)用 wait()和 notify()產(chǎn)生的問(wèn)題是,JVM/編譯器內(nèi)部會(huì)把常量字符串轉(zhuǎn)換成同一個(gè)對(duì)象。這意味著,即使你有 2 個(gè)不同的 MyWaitNotify 實(shí)例,它們都引用了相同的空字符串實(shí)例。同時(shí)也意味著存在這樣的風(fēng)險(xiǎn):在第一個(gè) MyWaitNotify 實(shí)例上調(diào)用 doWait()的線程會(huì)被在第二個(gè) MyWaitNotify 實(shí)例上調(diào)用 doNotify()的線程喚醒。這種情況可以畫成以下這張圖:
起初這可能不像個(gè)大問(wèn)題。畢竟,如果 doNotify()在第二個(gè) MyWaitNotify 實(shí)例上被調(diào)用,真正發(fā)生的事不外乎線程 A 和 B 被錯(cuò)誤的喚醒了 。這個(gè)被喚醒的線程(A 或者 B)將在 while 循環(huán)里檢查信號(hào)值,然后回到等待狀態(tài),因?yàn)?doNotify()并沒(méi)有在第一個(gè) MyWaitNotify 實(shí)例上調(diào)用,而這個(gè)正是它要等待的實(shí)例。這種情況相當(dāng)于引發(fā)了一次假喚醒。線程 A 或者 B 在信號(hào)值沒(méi)有更新的情況下喚醒。但是代碼處理了這種情況,所以線程回到了等待狀態(tài)。記住,即使 4 個(gè)線程在相同的共享字符串實(shí)例上調(diào)用 wait()和 notify(),doWait()和 doNotify()里的信號(hào)還會(huì)被 2 個(gè) MyWaitNotify 實(shí)例分別保存。在 MyWaitNotify1 上的一次 doNotify()調(diào)用可能喚醒 MyWaitNotify2 的線程,但是信號(hào)值只會(huì)保存在 MyWaitNotify1 里。
問(wèn)題在于,由于 doNotify()僅調(diào)用了 notify()而不是 notifyAll(),即使有 4 個(gè)線程在相同的字符串(空字符串)實(shí)例上等待,只能有一個(gè)線程被喚醒。所以,如果線程 A 或 B 被發(fā)給 C 或 D 的信號(hào)喚醒,它會(huì)檢查自己的信號(hào)值,看看有沒(méi)有信號(hào)被接收到,然后回到等待狀態(tài)。而 C 和 D 都沒(méi)被喚醒來(lái)檢查它們實(shí)際上接收到的信號(hào)值,這樣信號(hào)便丟失了。這種情況相當(dāng)于前面所說(shuō)的丟失信號(hào)的問(wèn)題。C 和 D 被發(fā)送過(guò)信號(hào),只是都不能對(duì)信號(hào)作出回應(yīng)。
如果 doNotify()方法調(diào)用 notifyAll(),而非 notify(),所有等待線程都會(huì)被喚醒并依次檢查信號(hào)值。線程 A 和 B 將回到等待狀態(tài),但是 C 或 D 只有一個(gè)線程注意到信號(hào),并退出 doWait()方法調(diào)用。C 或 D 中的另一個(gè)將回到等待狀態(tài),因?yàn)楂@得信號(hào)的線程在退出 doWait()的過(guò)程中清除了信號(hào)值(置為 false)。
看過(guò)上面這段后,你可能會(huì)設(shè)法使用 notifyAll()來(lái)代替 notify(),但是這在性能上是個(gè)壞主意。在只有一個(gè)線程能對(duì)信號(hào)進(jìn)行響應(yīng)的情況下,沒(méi)有理由每次都去喚醒所有線程。
所以:在 wait()/notify()機(jī)制中,不要使用全局對(duì)象,字符串常量等。應(yīng)該使用對(duì)應(yīng)唯一的對(duì)象。例如,每一個(gè) MyWaitNotify3 的實(shí)例擁有一個(gè)屬于自己的監(jiān)視器對(duì)象,而不是在空字符串上調(diào)用 wait()/notify()。
以上就是關(guān)于Java 多線程,線程通信的資料整理,后續(xù)繼續(xù)補(bǔ)充相關(guān)資料,謝謝大家對(duì)本站的支持!
相關(guān)文章
intellij idea如何將web項(xiàng)目打成war包的實(shí)現(xiàn)
這篇文章主要介紹了intellij idea如何將web項(xiàng)目打成war包的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Java 進(jìn)程執(zhí)行外部程序造成阻塞的一種原因
前一陣子在研究文檔展示時(shí)使用了java進(jìn)程直接調(diào)用外部程序,其中遇到一個(gè)問(wèn)題花了好長(zhǎng)時(shí)間才解決,這個(gè)問(wèn)題就是外部程序直接執(zhí)行沒(méi)什么問(wèn)題,但是當(dāng)使用Java進(jìn)程執(zhí)行時(shí)外部程序就阻塞在那兒不動(dòng)了。而且這個(gè)外部程序在處理某些文件時(shí)使用Java進(jìn)程執(zhí)行是沒(méi)問(wèn)題的2014-03-03詳解Spring Aop實(shí)例之AspectJ注解配置
本篇文章主要介紹了詳解Spring Aop實(shí)例之AspectJ注解配置,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04jsp頁(yè)面中獲取servlet請(qǐng)求中的參數(shù)的辦法詳解
在JAVA WEB應(yīng)用中,如何獲取servlet請(qǐng)求中的參數(shù),本文講解了jsp頁(yè)面中獲取servlet請(qǐng)求中的參數(shù)的辦法2018-03-03Java使用正則表達(dá)式驗(yàn)證手機(jī)號(hào)和電話號(hào)碼的方法
今天小編就為大家分享一篇關(guān)于Java使用正則表達(dá)式驗(yàn)證手機(jī)號(hào)和電話號(hào)碼的方法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12