線程通信的目标是使線程間能夠互相發送信号。另一方面,線程通信使線程能夠等待其他線程的信号。
例如,線程b可以等待線程a的一個信号,這個信号會通知線程b資料已經準備好了。本文将講解以下幾個java線程間通信的主題:
線程間發送信号的一個簡單方式是在共享對象的變量裡設定信号值。線程a在一個同步塊裡設定boolean型成員變量
hasdatatoprocess為true,線程b也在同步塊裡讀取hasdatatoprocess這個成員變量。這個簡單的例子使用了一個持有信号
的對象,并提供了set和check方法:
<code>01</code>
<code>public</code> <code>class</code> <code>mysignal{</code>
<code>02</code>
<code>03</code>
<code> </code><code>protected</code> <code>boolean</code> <code>hasdatatoprocess =</code><code>false</code><code>;</code>
<code>04</code>
<code>05</code>
<code> </code><code>public</code> <code>synchronized</code> <code>boolean</code> <code>hasdatatoprocess(){</code>
<code>06</code>
<code> </code><code>return</code> <code>this</code><code>.hasdatatoprocess;</code>
<code>07</code>
<code> </code><code>}</code>
<code>08</code>
<code>09</code>
<code> </code><code>public</code> <code>synchronized</code> <code>void</code> <code>sethasdatatoprocess(</code><code>boolean</code> <code>hasdata){</code>
<code>10</code>
<code> </code><code>this</code><code>.hasdatatoprocess = hasdata;</code>
<code>11</code>
<code>12</code>
<code>13</code>
<code>}</code>
線程a和b必須獲得指向一個mysignal共享執行個體的引用,以便進行通信。如果它們持有的引用指向不同的mysingal執行個體,那麼彼此将不能檢測到對方的信号。需要處理的資料可以存放在一個共享緩存區裡,它和mysignal執行個體是分開存放的。
準備處理資料的線程b正在等待資料變為可用。換句話說,它在等待線程a的一個信号,這個信号使hasdatatoprocess()傳回true。線程b運作在一個循環裡,以等待這個信号:
<code>1</code>
<code>protected</code> <code>mysignal sharedsignal = ...</code>
<code>2</code>
<code>3</code>
<code>...</code>
<code>4</code>
<code>5</code>
<code>while</code><code>(!sharedsignal.hasdatatoprocess()){</code>
<code>6</code>
<code> </code><code>//do nothing... busy waiting</code>
<code>7</code>
忙等待沒有對運作等待線程的cpu進行有效的利用,除非平均等待時間非常短。否則,讓等待線程進入睡眠或者非運作狀态更為明智,直到它接收到它等待的信号。
java有一個内建的等待機制來允許線程在等待信号的時候變為非運作狀态。java.lang.object 類定義了三個方法,wait()、notify()和notifyall()來實作這個等待機制。
一個線程一旦調用了任意對象的wait()方法,就會變為非運作狀态,直到另一個線程調用了同一個對象的notify()方法。為了調用
wait()或者notify(),線程必須先獲得那個對象的鎖。也就是說,線程必須在同步塊裡調用wait()或者notify()。以下是
mysingal的修改版本——使用了wait()和notify()的mywaitnotify:
<code>public</code> <code>class</code> <code>monitorobject{</code>
<code>public</code> <code>class</code> <code>mywaitnotify{</code>
<code> </code><code>monitorobject mymonitorobject =</code><code>new</code> <code>monitorobject();</code>
<code> </code><code>public</code> <code>void</code> <code>dowait(){</code>
<code> </code><code>synchronized</code><code>(mymonitorobject){</code>
<code> </code><code>try</code><code>{</code>
<code> </code><code>mymonitorobject.wait();</code>
<code> </code><code>}</code><code>catch</code><code>(interruptedexception e){...}</code>
<code> </code><code>}</code>
<code>14</code>
<code>15</code>
<code>16</code>
<code> </code><code>public</code> <code>void</code> <code>donotify(){</code>
<code>17</code>
<code>18</code>
<code> </code><code>mymonitorobject.notify();</code>
<code>19</code>
<code>20</code>
<code>21</code>
等待線程将調用dowait(),而喚醒線程将調用donotify()。當一個線程調用一個對象的notify()方法,正在等待該對象的所有線
程中将有一個線程被喚醒并允許執行(校注:這個将被喚醒的線程是随機的,不可以指定喚醒哪個線程)。同時也提供了一個notifyall()方法來喚醒正
在等待一個給定對象的所有線程。
如你所見,不管是等待線程還是喚醒線程都在同步塊裡調用wait()和notify()。這是強制性的!一個線程如果沒有持有對象鎖,将不能調用
wait(),notify()或者notifyall()。否則,會抛出illegalmonitorstateexception異常。
但是,這怎麼可能?等待線程在同步塊裡面執行的時候,不是一直持有螢幕對象(mymonitor對象)的鎖嗎?等待線程不能阻塞喚醒線程進入
donotify()的同步塊嗎?答案是:的确不能。一旦線程調用了wait()方法,它就釋放了所持有的螢幕對象上的鎖。這将允許其他線程也可以調用
wait()或者notify()。
一旦一個線程被喚醒,不能立刻就退出wait()的方法調用,直到調用notify()的線程退出了它自己的同步塊。換句話說:被喚醒的線程必須重
新獲得螢幕對象的鎖,才可以退出wait()的方法調用,因為wait方法調用運作在同步塊裡面。如果多個線程被notifyall()喚醒,那麼在同
一時刻将隻有一個線程可以退出wait()方法,因為每個線程在退出wait()前必須獲得螢幕對象的鎖。
notify()和notifyall()方法不會儲存調用它們的方法,因為當這兩個方法被調用時,有可能沒有線程處于等待狀态。通知信号過後便丢
棄了。是以,如果一個線程先于被通知線程調用wait()前調用了notify(),等待的線程将錯過這個信号。這可能是也可能不是個問題。不過,在某些
情況下,這可能使等待線程永遠在等待,不再醒來,因為線程錯過了喚醒信号。
為了避免丢失信号,必須把它們儲存在信号類裡。在mywaitnotify的例子中,通知信号應被存儲在mywaitnotify執行個體的一個成員變量裡。以下是mywaitnotify的修改版本:
<code>public</code> <code>class</code> <code>mywaitnotify2{</code>
<code> </code><code>boolean</code> <code>wassignalled =</code><code>false</code><code>;</code>
<code> </code><code>if</code><code>(!wassignalled){</code>
<code> </code><code>try</code><code>{</code>
<code> </code><code>mymonitorobject.wait();</code>
<code> </code><code>}</code><code>catch</code><code>(interruptedexception e){...}</code>
<code> </code><code>}</code>
<code> </code><code>//clear signal and continue running.</code>
<code> </code><code>wassignalled =</code><code>false</code><code>;</code>
<code> </code><code>wassignalled =</code><code>true</code><code>;</code>
<code>22</code>
<code>23</code>
<code>24</code>
留意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的修改版本展示了這點:
<code>public</code> <code>class</code> <code>mywaitnotify3{</code>
<code> </code><code>while</code><code>(!wassignalled){</code>
留意wait()方法是在while循環裡,而不在if表達式裡。如果等待線程沒有收到信号就喚醒,wassignalled變量将變為false,while循環會再執行一次,促使醒來的線程回到等待狀态。
如果你有多個線程在等待,被notifyall()喚醒,但隻有一個被允許繼續執行,使用while循環也是個好方法。每次隻有一個線程可以獲得監
視器對象鎖,意味着隻有一個線程可以退出wait()調用并清除wassignalled标志(設為false)。一旦這個線程退出dowait()的同
步塊,其他線程退出wait()調用,并在while循環裡檢查wassignalled變量值。但是,這個标志已經被第一個喚醒的線程清除了,是以其餘
醒來的線程将回到等待狀态,直到下次信号到來。
(校注:本章說的字元串常量指的是值為常量的變量)
本文早期的一個版本在mywaitnotify例子裡使用字元串常量(””)作為管程對象。以下是那個例子:
<code> </code><code>string mymonitorobject =</code><code>""</code><code>;</code>
在空字元串作為鎖的同步塊(或者其他常量字元串)裡調用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(),但是這在性能上是個壞主意。在隻有一個線程能對信号進行響應的情況下,沒有理由每次都去喚醒所有線程。
校注:
管程 (英語:monitors,也稱為螢幕)
是對多個工作線程實作互斥通路共享資源的對象或子產品。這些共享資源一般是硬體裝置或一群變量。管程實作了在一個時間點,最多隻有一個線程在執行它的某個子
程式。與那些通過修改資料結構實作互斥通路的并發程式設計相比,管程很大程度上簡化了程式設計。