首頁 > Java > java教程 > 主體

java 多執行緒-執行緒通訊實例講解

高洛峰
發布: 2017-01-05 15:08:05
原創
1079 人瀏覽過

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

透過共享物件通訊

忙等待

wait(),notify()和notifyAll()

丟失的訊號

假喚醒

多執行相同訊號

。 wait()


透過共享物件通訊

執行緒間發送訊號的一個簡單方式是在共享物件的變數裡設定訊號值。執行緒 A 在一個同步區塊裡設定 boolean 型成員變數 hasDataToProcess 為 true,執行緒 B 也在同步區塊裡讀取 hasDataToProcess 這個成員變數。這個簡單的例子使用了一個持有訊號的對象,並提供了 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 必須獲得指向一個 MySignal 共享實例的引用,以便進行通信。如果它們所持有的引用指向不同的 MySingal 實例,那麼彼此將無法偵測到對方的訊號。需要處理的資料可以存放在一個共享快取區裡,它和 MySignal 實例是分開存放的。

忙等待(Busy Wait)

準備處理資料的執行緒 B 正在等待資料變成可用。換句話說,它在等待線程 A 的一個訊號,這個訊號使 hasDataToProcess()回傳 true。執行緒 B 運行在一個循環裡,以等待這個訊號:

protected MySignal sharedSignal = ...
 
...
 
while(!sharedSignal.hasDataToProcess()){
 //do nothing... busy waiting
}
登入後複製

wait(),notify()和 notifyAll()

忙等待沒有對運行等待執行緒的 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。)

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

一旦一個執行緒被喚醒,不能立刻就退出 wait()的方法調用,直到調用 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();
 }
 }
}
 
<br>
登入後複製

執行緒退出了它自己的同步區塊。換句話說:被喚醒的執行緒必須重新獲得監視器物件的鎖,才可以退出 wait()的方法調用,因為 wait 方法調用運行在同步區塊裡面。如果多個執行緒被 notifyAll()喚醒,那麼在同一時刻將只有一個執行緒可以退出 wait()方法,因為每個執行緒在退出 wait()前必須獲得監視器物件的鎖定。

遺失的訊號(Missed Signals)

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 变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。

不要在字符串常量或全局对象中调用 wait()

(校注:本章说的字符串常量指的是值为常量的变量)

本文早期的一个版本在 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()的线程唤醒。这种情况可以画成以下这张图:

java 多线程-线程通信实例讲解

起初这可能不像个大问题。毕竟,如果 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()。

以上就是關於Java 多線程,線程通訊的資料整理,後續繼續補充相關資料,謝謝大家對本站的支持!

更多java 多執行緒-執行緒通訊實例講解相關文章請關注PHP中文網!


相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板