搜尋
首頁Javajava教程java 多執行緒-執行緒通訊實例講解

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

透過共享物件通訊

忙等待

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中文網!


陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),