線程通訊的目標是使線程間可以互相發送信號。另外一方面,線程通訊使線程可以等待其餘線程的信號。html
例如,線程B能夠等待線程A的一個信號,這個信號會通知線程B數據已經準備好了。本文將講解如下幾個JAVA線程間通訊的主題:java
一、經過共享對象通訊segmentfault
二、忙等待緩存
三、wait(),notify()和notifyAll()數據結構
四、丟失的信號多線程
五、假喚醒併發
六、多線程等待相同信號性能
七、不要對常量字符串或全局對象調用wait()
this
線程間發送信號的一個簡單方式是在共享對象的變量裏設置信號值。線程A在一個同步塊裏設置boolean型成員變量hasDataToProcess爲true,線程B也在同步塊裏讀取hasDataToProcess這個成員變量。這個簡單的例子使用了一個持有信號的對象,並提供了set和check方法:spa
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必須得到指向一個MySignal共享實例的引用,以便進行通訊。若是它們持有的引用指向不一樣的MySingal實例,那麼彼此將不能檢測到對方的信號。須要處理的數據能夠存放在一個共享緩存區裏,它和MySignal實例是分開存放的。
準備處理數據的線程B正在等待數據變爲可用。換句話說,它在等待線程A的一個信號,這個信號使hasDataToProcess()返回true。線程B運行在一個循環裏,以等待這個信號:
protected MySignal sharedSignal = ... ... while(!sharedSignal.hasDataToProcess()){ //do nothing... busy waiting }
忙等待沒有對運行等待線程的CPU進行有效的利用,除非平均等待時間很是短。不然,讓等待線程進入睡眠或者非運行狀態更爲明智,直到它接收到它等待的信號。
Java有一個內建的等待機制來容許線程在等待信號的時候變爲非運行狀態。java.lang.Object 類定義了三個方法,wait()、notify()和notifyAll()來實現這個等待機制。
一個線程一旦調用了任意對象的wait()方法,就會變爲非運行狀態,直到另外一個線程調用了同一個對象的notify()方法。爲了調用wait()或者notify(),線程必須先得到那個對象的鎖。也就是說,線程必須在同步塊裏調用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(); } } }
等待線程將調用doWait(),而喚醒線程將調用doNotify()。當一個線程調用一個對象的notify()方法,正在等待該對象的全部線程中將有一個線程被喚醒並容許執行(校注:這個將被喚醒的線程是隨機的,不能夠指定喚醒哪一個線程)。同時也提供了一個notifyAll()方法來喚醒正在等待一個給定對象的全部線程。
如你所見,不論是等待線程仍是喚醒線程都在同步塊裏調用wait()和notify()。這是強制性的!一個線程若是沒有持有對象鎖,將不能調用wait(),notify()或者notifyAll()。不然,會拋出IllegalMonitorStateException異常。
(校注:JVM是這麼實現的,當你調用wait時候它首先要檢查下當前線程是不是鎖的擁有者,不是則拋出IllegalMonitorStateExcept,參考JVM源碼的 1422行。)
可是,這怎麼可能?等待線程在同步塊裏面執行的時候,不是一直持有監視器對象(myMonitor對象)的鎖嗎?等待線程不能阻塞喚醒線程進入doNotify()的同步塊嗎?答案是:的確不能。一旦線程調用了wait()方法,它就釋放了所持有的監視器對象上的鎖。這將容許其餘線程也能夠調用wait()或者notify()。
一旦一個線程被喚醒,不能馬上就退出wait()的方法調用,直到調用notify()的線程退出了它本身的同步塊。換句話說:被喚醒的線程必須從新得到監視器對象的鎖,才能夠退出wait()的方法調用,由於wait方法調用運行在同步塊裏面。若是多個線程被notifyAll()喚醒,那麼在同一時刻將只有一個線程能夠退出wait()方法,由於每一個線程在退出wait()前必須得到監視器對象的鎖。
notify()和notifyAll()方法不會保存調用它們的方法,由於當這兩個方法被調用時,有可能沒有線程處於等待狀態。通知信號事後便丟棄了。所以,若是一個線程先於被通知線程調用wait()前調用了notify(),等待的線程將錯過這個信號。這多是也可能不是個問題。不過,在某些狀況下,這可能使等待線程永遠在等待,再也不醒來,由於線程錯過了喚醒信號。
爲了不丟失信號,必須把它們保存在信號類裏。在MyWaitNotify的例子中,通知信號應被存儲在MyWaitNotify實例的一個成員變量裏。如下是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()方法在調用notify()前把wasSignalled變量設爲true。同時,留意doWait()方法在調用wait()前會檢查wasSignalled變量。事實上,若是沒有信號在前一次doWait()調用和此次doWait()調用之間的時間段裏被接收到,它將只調用wait()。
(校注:爲了不信號丟失, 用一個變量來保存是否被通知過。在notify前,設置本身已經被通知過。在wait後,設置本身沒有被通知過,須要等待通知。)
因爲莫名其妙的緣由,線程有可能在沒有調用過notify()和notifyAll()的狀況下醒來。這就是所謂的假喚醒(spurious wakeups)。無故端地醒過來了。
若是在MyWaitNotify2的doWait()方法裏發生了假喚醒,等待線程即便沒有收到正確的信號,也可以執行後續的操做。這可能致使你的應用程序出現嚴重問題。
爲了防止假喚醒,保存信號的成員變量將在一個while循環裏接受檢查,而不是在if表達式裏。這樣的一個while循環叫作自旋鎖(校注:這種作法要慎重,目前的JVM實現自旋會消耗CPU,若是長時間不調用doNotify方法,doWait方法會一直自旋,CPU會消耗太大)。被喚醒的線程會自旋直到自旋鎖(while循環)裏的條件變爲false。如下MyWaitNotify2的修改版本展現了這點:
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循環裏,而不在if表達式裏。若是等待線程沒有收到信號就喚醒,wasSignalled變量將變爲false,while循環會再執行一次,促使醒來的線程回到等待狀態。
若是你有多個線程在等待,被notifyAll()喚醒,但只有一個被容許繼續執行,使用while循環也是個好方法。每次只有一個線程能夠得到監視器對象鎖,意味着只有一個線程能夠退出wait()調用並清除wasSignalled標誌(設爲false)。一旦這個線程退出doWait()的同步塊,其餘線程退出wait()調用,並在while循環裏檢查wasSignalled變量值。可是,這個標誌已經被第一個喚醒的線程清除了,因此其他醒來的線程將回到等待狀態,直到下次信號到來。
(校注:本章說的字符串常量指的是值爲常量的變量)
本文早期的一個版本在MyWaitNotify例子裏使用字符串常量(」")做爲管程對象。如下是那個例子:
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(); } } }
在空字符串做爲鎖的同步塊(或者其餘常量字符串)裏調用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的信號喚醒,它會檢查本身的信號值,看看有沒有信號被接收到,而後回到等待狀態。而C和D都沒被喚醒來檢查它們實際上接收到的信號值,這樣信號便丟失了。這種狀況至關於前面所說的丟失信號的問題。C和D被髮送過信號,只是都不能對信號做出迴應。
若是doNotify()方法調用notifyAll(),而非notify(),全部等待線程都會被喚醒並依次檢查信號值。線程A和B將回到等待狀態,可是C或D只有一個線程注意到信號,並退出doWait()方法調用。C或D中的另外一個將回到等待狀態,由於得到信號的線程在退出doWait()的過程當中清除了信號值(置爲false)。
看過上面這段後,你可能會設法使用notifyAll()來代替notify(),可是這在性能上是個壞主意。在只有一個線程能對信號進行響應的狀況下,沒有理由每次都去喚醒全部線程。
因此:在wait()/notify()機制中,不要使用全局對象,字符串常量等。應該使用對應惟一的對象。例如,每個MyWaitNotify3的實例(前一節的例子)擁有一個屬於本身的監視器對象,而不是在空字符串上調用wait()/notify()。
校注:
<
p>管程 (英語:Monitors,也稱爲監視器) 是對多個工做線程實現互斥訪問共享資源的對象或模塊。這些共享資源通常是硬件設備或一羣變量。管程實現了在一個時間點,最多隻有一個線程在執行它的某個子程序。與那些經過修改數據結構實現互斥訪問的併發程序設計相比,管程很大程度上簡化了程序設計。
<
p>
原文 Thread Signaling
譯者:杜建雄 校對:方騰飛
via ifeve