當前位置:
首頁 > 最新 > Java線程通信的目標是使線程間能夠互相發送信號

Java線程通信的目標是使線程間能夠互相發送信號

線程通信

線程通信的目標是使線程間能夠互相發送信號。另一方面,線程通信使線程能夠等待其他線程的信號。

例如,線程B可以等待線程A的一個信號,這個信號會通知線程B數據已經準備好了。本文將講解以下幾個JAVA線程間通信的主題:

1、通過共享對象通信

2、忙等待

3、wait(),notify()和notifyAll()

4、丟失的信號

5、假喚醒

6、多線程等待相同信號

7、不要對常量字元串或全局對象調用wait()

1、通過共享對象通信

線程間發送信號的一個簡單方式是在共享對象的變數里設置信號值。線程A在一個同步塊里設置boolean型成員變數hasDataToProcess為true,線程B也在同步塊里讀取hasDataToProcess這個成員變數。這個簡單的例子使用了一個持有信號的對象,並提供了set和check方法:

[java] view plain copy

publicclass MySignal{

protectedboolean hasDataToProcess = false;

publicsynchronizedboolean hasDataToProcess(){

returnthis.hasDataToProcess;

}

publicsynchronizedvoid setHasDataToProcess(boolean hasData){

this.hasDataToProcess = hasData;

}

}

線程A和B必須獲得指向一個MySignal共享實例的引用,以便進行通信。如果它們持有的引用指向不同的MySingal實例,那麼彼此將不能檢測到對方的信號。需要處理的數據可以存放在一個共享緩存區里,它和MySignal實例是分開存放的。

2、忙等待(Busy Wait)

準備處理數據的線程B正在等待數據變為可用。換句話說,它在等待線程A的一個信號,這個信號使hasDataToProcess()返回true。線程B運行在一個循環里,以等待這個信號:

[java] view plain copy

protected MySignal sharedSignal = ...

...

while(!sharedSignal.hasDataToProcess()){

//do nothing... busy waiting

}

3、wait(),notify()和notifyAll()

忙等待沒有對運行等待線程的CPU進行有效的利用,除非平均等待時間非常短。否則,讓等待線程進入睡眠或者非運行狀態更為明智,直到它接收到它等待的信號。

Java有一個內建的等待機制來允許線程在等待信號的時候變為非運行狀態。java.lang.Object 類定義了三個方法,wait()、notify()和notifyAll()來實現這個等待機制。

一個線程一旦調用了任意對象的wait()方法,就會變為非運行狀態,直到另一個線程調用了同一個對象的notify()方法。為了調用wait()或者notify(),線程必須先獲得那個對象的鎖。也就是說,線程必須在同步塊里調用wait()或者notify()。以下是MySingal的修改版本——使用了wait()和notify()的MyWaitNotify:

[java] view plain copy

publicclass MonitorObject{

}

publicclass MyWaitNotify{

MonitorObject myMonitorObject = new MonitorObject();

publicvoid doWait(){

synchronized(myMonitorObject){

try{

myMonitorObject.wait();

} catch(InterruptedException e){...}

}

}

publicvoid doNotify(){

synchronized(myMonitorObject){

myMonitorObject.notify();

}

}

}

等待線程將調用doWait(),而喚醒線程將調用doNotify()。當一個線程調用一個對象的notify()方法,正在等待該對象的所有線程中將有一個線程被喚醒並允許執行(校註:這個將被喚醒的線程是隨機的,不可以指定喚醒哪個線程)。同時也提供了一個notifyAll()方法來喚醒正在等待一個給定對象的所有線程。

想要了解更多Java知識 加入學習群一四四九零一零七六 可以免費學習java還有大量學習乾貨哦

如你所見,不管是等待線程還是喚醒線程都在同步塊里調用wait()和notify()。這是強制性的!一個線程如果沒有持有對象鎖,將不能調用wait(),notify()或者notifyAll()。如果調用了,會拋出IllegalMonitorStateException異常。

(校註:JVM是這麼實現的,當你調用wait時候它首先要檢查下當前線程是否是鎖的擁有者,不是則拋出IllegalMonitorStateExcept,參考JVM源碼的 1422行。)

但是,這怎麼可能?等待線程在同步塊裡面執行的時候,不是一直持有監視器對象(myMonitor對象)的鎖嗎?等待線程不能阻止喚醒線程進入doNotify()的同步塊嗎?答案是:的確不能。一旦線程調用了wait()方法,它就釋放了所持有的監視器對象上的鎖。這將允許其他線程也可以調用wait()或者notify()。

一旦一個線程被喚醒,不能立刻就退出wait()的方法調用,直到調用notify()的線程退出了它自己的同步塊。換句話說:被喚醒的線程必須重新獲得監視器對象的鎖,才可以退出wait()的方法調用,因為wait方法調用運行在同步塊裡面。如果多個線程被notifyAll()喚醒,那麼在同一時刻將只有一個線程可以退出wait()方法,因為每個線程在退出wait()前必須獲得監視器對象的鎖。

4、丟失的信號(Missed Signals)

notify()和notifyAll()方法不會保存調用它們的方法,因為當這兩個方法被調用時,有可能沒有線程處於等待狀態。通知信號過後便丟棄了。因此,如果一個線程先於被通知線程調用wait()前調用了notify(),等待的線程將錯過這個信號。這可能是也可能不是個問題。不過,在某些情況下,這可能使等待線程永遠在等待,不再醒來,因為線程錯過了喚醒信號。

為了避免丟失信號,必須把它們保存在信號類里。在MyWaitNotify的例子中,通知信號應被存儲在MyWaitNotify實例的一個成員變數里。以下是MyWaitNotify的修改版本:

[java] view plain copy

publicclass MyWaitNotify2{

MonitorObject myMonitorObject = new MonitorObject();

boolean wasSignalled = false;

publicvoid doWait(){

synchronized(myMonitorObject){

if(!wasSignalled){

try{

myMonitorObject.wait();

} catch(InterruptedException e){...}

}

//clear signal and continue running.

wasSignalled = false;

}

}

publicvoid doNotify(){

synchronized(myMonitorObject){

wasSignalled = true;

myMonitorObject.notify();

}

}

}

留意doNotify()方法在調用notify()前把wasSignalled變數設為true。同時,留意doWait()方法在調用wait()前會檢查wasSignalled變數。事實上,如果沒有信號在前一次doWait()調用和這次doWait()調用之間的時間段里被接收到,它將只調用wait()。

(校註:為了避免信號丟失, 用一個變數來保存是否被通知過。在notify前,設置自己已經被通知過。在wait後,設置自己沒有被通知過,需要等待通知。)

5、假喚醒

由於莫名其妙的原因,線程有可能在沒有調用過notify()和notifyAll()的情況下醒來。這就是所謂的假喚醒(spurious wakeups)。無端端地醒過來了。

如果在MyWaitNotify2的doWait()方法里發生了假喚醒,等待線程即使沒有收到正確的信號,也能夠執行後續的操作。這可能導致你的應用程序出現嚴重問題。

為了防止假喚醒,保存信號的成員變數將在一個while循環里接受檢查,而不是在if表達式里。這樣的一個while循環叫做自旋鎖(校註:這種做法要慎重,目前的JVM實現自旋會消耗CPU,如果長時間不調用doNotify方法,doWait方法會一直自旋,CPU會消耗太大)。被喚醒的線程會自旋直到自旋鎖(while循環)里的條件變為false。以下MyWaitNotify2的修改版本展示了這點:

[java] view plain copy

publicclass MyWaitNotify3{

MonitorObject myMonitorObject = new MonitorObject();

boolean wasSignalled = false;

publicvoid doWait(){

synchronized(myMonitorObject){

while(!wasSignalled){

try{

myMonitorObject.wait();

} catch(InterruptedException e){...}

}

//clear signal and continue running.

wasSignalled = false;

}

}

publicvoid doNotify(){

synchronized(myMonitorObject){

wasSignalled = true;

myMonitorObject.notify();

}

}

}

留意wait()方法是在while循環里,而不在if表達式里。如果等待線程沒有收到信號就喚醒,wasSignalled變數將變為false,while循環會再執行一次,促使醒來的線程回到等待狀態。

6、多個線程等待相同信號

如果你有多個線程在等待,被notifyAll()喚醒,但只有一個被允許繼續執行,使用while循環也是個好方法。每次只有一個線程可以獲得監視器對象鎖,意味著只有一個線程可以退出wait()調用並清除wasSignalled標誌(設為false)。一旦這個線程退出doWait()中的同步塊,其他線程就可以退出wait()調用,並在while循環里檢查wasSignalled變數值。但是,這個標誌已經被第一個喚醒的線程清除了,所以其餘醒來的線程將回到等待狀態,直到下次信號到來。

7、不要在字元串常量或全局對象中調用wait()

(校註:本章說的字元串常量指的是值為常量的字元串變數)

本文早期的一個版本在MyWaitNotify例子里使用字元串常量(」」)作為管程對象(即監視器對象)。以下是那個例子:

[java] view plain copy

publicclass MyWaitNotify{

String myMonitorObject = "";

boolean wasSignalled = false;

publicvoid doWait(){

synchronized(myMonitorObject){

while(!wasSignalled){

try{

myMonitorObject.wait();

} catch(InterruptedException e){...}

}

//clear signal and continue running.

wasSignalled = false;

}

}

publicvoid doNotify(){

synchronized(myMonitorObject){

wasSignalled = true;

myMonitorObject.notify();

}

}

}

在空字元串作為鎖的同步塊(或者其他常量字元串)里調用wait()和notify()產生的問題是,JVM/編譯器內部會把常量字元串轉換成同一個對象。這意味著,即使你有2個不同的MyWaitNotify實例,它們都引用了相同的空字元串實例。同時也意味著存在這樣的風險:在第一個MyWaitNotify實例上調用doWait()的線程會被在第二個MyWaitNotify實例上調用doNotify()的線程喚醒。這種情況可以畫成以下這張圖:

起初這可能不像個大問題。畢竟,如果doNotify()在第二個MyWaitNotify實例上被調用,真正發生的事不外乎線程A和B被錯誤的喚醒了 。這個被喚醒的線程(A或者B)將在while循環里檢查信號值,然後回到等待狀態,因為doNotify()並沒有在第一個MyWaitNotify實例上調用,而這個正是它要等待的實例。這種情況相當於引發了一次假喚醒。線程A或者B在信號值沒有更新的情況下喚醒。但是代碼處理了這種情況,所以線程回到了等待狀態。記住,即使4個線程在相同的共享字元串實例上調用wait()和notify(),doWait()和doNotify()里的信號還會被2個MyWaitNotify實例分別保存。在MyWaitNotify1上的一次doNotify()調用可能喚醒MyWaitNotify2的線程,但是信號值只會保存在MyWaitNotify1里。

問題在於,由於doNotify()僅調用了notify()而不是notifyAll(),即使有4個線程在相同的字元串(空字元串)實例上等待,只能有一個線程被喚醒。所以,如果線程A或B被喚醒但信號是發給C或D的,A或B會檢查自己的信號值,看看有沒有信號被接收到,然後回到等待狀態。而C和D都沒被喚醒來檢查它們實際上接收到的信號值,這樣信號便丟失了。這種情況相當於前面所說的丟失信號的問題。C和D被發送過信號,只是都不能對信號作出回應。

如果doNotify()方法調用notifyAll(),而非notify(),所有等待線程都會被喚醒並依次檢查信號值。線程A和B將回到等待狀態,但是C或D只有一個線程注意到信號,並退出doWait()方法調用。C或D中的另一個將回到等待狀態,因為獲得信號的線程在退出doWait()的過程中清除了信號值(置為false)。

看過上面這段後,你可能會設法使用notifyAll()來代替notify(),但是這在性能上是個壞主意。在只有一個線程能對信號進行響應的情況下,沒有理由每次都去喚醒所有線程。

所以:在wait()/notify()機制中,不要使用全局對象,字元串常量等。應該使用對應唯一的對象。例如,每一個MyWaitNotify3的實例(前一節的例子)擁有一個屬於自己的監視器對象,而不是在空字元串上調用wait()/notify()。

校註:

管程 (英語:Monitors,也稱為監視器) 是對多個工作線程實現互斥訪問共享資源的對象或模塊。這些共享資源一般是硬體設備或一群變數。管程實現了在一個時間點,最多只有一個線程在執行它的某個子程序。與那些通過修改數據結構實現互斥訪問的並發程序設計相比,管程很大程度上簡化了程序設計。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 IT技術java交流 的精彩文章:

Java並發與多線程圖文教程
學習JAVA可以從事哪些崗位?
Java核心技術——繼承
學習筆記——Java核心技術之介面、繼承與多態練習題
Java程序員面試失敗的5大原因

TAG:IT技術java交流 |

您可能感興趣

盤點嵌入式Linux中進程間通信和線程間通信的幾種方式
Linux 下的進程間通信:套接字和信號
進程間通信-Queue
七大進程間通信和線程同步
Linux 下的進程間通信:共享存儲
Linux講解 進程間通信 命名管道
一文讀懂python進程間通信之匿名管道
Linux 下的進程間通信:使用管道和消息隊列
進程間的通信 IPC——實現消息隊列(msg)
Android 中通過 AIDL 完成進程間通信