執行緒之間的通訊(thread signal)
執行緒通訊的目的是為了能夠讓執行緒之間相互發送訊號。另外,執行緒通訊還能夠使得執行緒等待其它執行緒的訊號,比如,執行緒B可以等待執行緒A的訊號,這個訊號可以是執行緒A已經處理完成的訊號。
通過共享物件通訊
有一個簡單的實現執行緒之間通訊的方式,就是在共享物件的變數中設定訊號值。比如執行緒A在一個同步塊中設定一個成員變數hasDataToProcess
值為true
,而執行緒B同樣在一個同步塊中讀取這個成員變數。下面例子演示了一個持有訊號值的物件,並提供了設定訊號值和獲取訊號值的同步方法:
public class MySignal { private boolean hasDataToProcess; public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess=hasData; } public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; } }
ThreadB計算完成會在共享物件中設定訊號值:
public class ThreadB extends Thread{ int count; MySignal mySignal; public ThreadB(MySignal mySignal){ this.mySignal=mySignal; } @Override public void run(){ for(int i=0;i<100;i++){ count=count+1; } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } mySignal.setHasDataToProcess(true); } }
ThreadA在迴圈中一直檢測共享物件的訊號值,等待ThreadB計算完成的訊號:
public class ThreadA extends Thread{ MySignal mySignal; ThreadB threadB; public ThreadA(MySignal mySignal, ThreadB threadB){ this.mySignal=mySignal; this.threadB=threadB; } @Override public void run(){ while (true){ if(mySignal.hasDataToProcess()){ System.out.println("執行緒B計算結果為:"+threadB.count); break; } } } public static void main(String[] args) { MySignal mySignal=new MySignal(); ThreadB threadB=new ThreadB(mySignal); ThreadA threadA=new ThreadA(mySignal,threadB); threadB.start(); threadA.start(); } }
很明顯,採用共享物件方式通訊的執行緒A和執行緒B必須持有同一個MySignal
物件的引用,這樣它們才能彼此檢測到對方設定的訊號。當然,訊號也可儲存在共享記憶體buffer中,它和例項是分開的。
執行緒的忙等
從上面例子中可以看出,執行緒A一直在等待資料就緒,或者說執行緒A一直在等待執行緒B設定hasDataToProcess
的訊號值為true:
public void run(){
while (true){
if(mySignal.hasDataToProcess()){
System.out.println("執行緒B計算結果為:"+threadB.count);
break;
}
}
}
為什麼說是忙等呢?因為上面程式碼一直在執行迴圈,直到hasDataToProcess
被設定為true。
忙等意味著執行緒還處於執行狀態,一直在消耗CPU資源,所以,忙等不是一種很好的現象。那麼能不能讓執行緒在等待訊號時釋放CPU資源進入阻塞狀態呢?其實java.lang.Object
提供的wait()、notify()、notifyAll()方法就可以解決忙等問題。
wait()、notify()、notifyAll()
Java提供了一種內聯機制可以讓執行緒在等待訊號時進入非執行狀態。當一個執行緒呼叫任何物件上的wait()方法時便會進入非執行狀態,直到另一個執行緒呼叫同一個物件上的notify()或notifyAll()方法。
為了能夠呼叫一個物件的wait()、notify()方法,呼叫執行緒必須先獲得這個物件的鎖。因為執行緒只有在同步塊中才會佔用物件的鎖,所以執行緒必須在同步塊中呼叫wait()、notify()方法。
我們把上面通過共享物件通訊的例子改成呼叫物件wait()、notify()方法來實現:
首先我們先構造一個任意物件,我們又把它稱作監控物件:
public class MonitorObject {
}
ThreadD負責計算,在計算完成時喚醒被阻塞的ThreadC:
public class ThreadD extends Thread{
int count;
MonitorObject mySignal;
public ThreadD(MonitorObject mySignal){
this.mySignal=mySignal;
}
@Override
public void run(){
for(int i=0;i<100;i++){
count=count+1;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mySignal){
mySignal.notify();//計算完成呼叫物件的notify()方法,喚醒因呼叫這個物件wait()方法而掛起的執行緒
}
}
}
ThreadC等待ThreadD的喚醒:
public class ThreadC extends Thread{
MonitorObject mySignal;
ThreadD threadD;
public ThreadC(MonitorObject mySignal, ThreadD threadD){
this.mySignal=mySignal;
this.threadD=threadD;
}
@Override
public void run(){
synchronized (mySignal){
try {
mySignal.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒B計算結果為:"+threadD.count);
}
}
public static void main(String[] args) {
MonitorObject mySignal=new MonitorObject();
ThreadD threadD=new ThreadD(mySignal);
ThreadC threadC=new ThreadC(mySignal,threadD);
threadC.start();
threadD.start();
}
}
在這個例子中,執行緒C因呼叫了監控物件的wait()方法而掛起,執行緒D通過呼叫監控物件的notify()方法喚醒掛起的執行緒C。我們還可以看到,兩個執行緒都是在同步塊中呼叫的wait()和notify()方法。如果一個執行緒在沒有獲得物件鎖的前提下呼叫了這個物件的wait()或notify()方法,方法呼叫時將會丟擲 IllegalMonitorStateException
異常。
注意,當一個執行緒呼叫一個物件的notify()方法,則會喚醒正在等待這個物件所有執行緒中的一個執行緒(喚醒的執行緒是隨機的),當執行緒呼叫的是物件的notifyAll()方法,則會喚醒所有等待這個物件的執行緒(喚醒的所有執行緒中哪一個會執行也是不確定的)。
這裡還有一個問題,既然呼叫物件wait()方法的執行緒需要獲得這個物件的鎖,那麼這會不會阻塞其它執行緒呼叫這個物件的notify()方法呢?答案是不會阻塞,當一個執行緒呼叫監控物件的wait()方法時,它便會釋放掉這個監控物件鎖,以便讓其它執行緒能夠呼叫這個物件的notify()方法或者wait()方法。
另外,當一個執行緒被喚醒時不會立刻退出wait()方法,只有當呼叫notify()的執行緒退出它的同步塊為止。也就是說,被喚醒的執行緒只有重新獲得監控物件鎖時才會退出wait()方法,因為wait()方法在同步塊中,它的執行需要再次獲得物件鎖。所以,當通過notifyAll()方法喚醒被阻塞的執行緒時,一次只能有一個執行緒會退出wait()方法,同樣是因為每個執行緒都需要先獲得監控物件鎖才能執行同步塊中的wait()方法退出。