Java執行緒詳解(6)-執行緒的互動
執行緒互動是比較複雜的問題,SCJP要求不很基礎:給定一個場景,編寫程式碼來恰當使用等待、通知和通知所有執行緒。
一、執行緒互動的基礎知識
SCJP所要求的執行緒互動知識點需要從java.lang.Object的類的三個方法來學習:
void notify()——喚醒在此物件監視器上等待的單個執行緒。
void notifyAll()——喚醒在此物件監視器上等待的所有執行緒。
void wait()——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法。
當然,wait()還有另外兩個過載方法:
void wait(longtimeout)——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
void wait(longtimeout, int nanos)——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。
以上這些方法是幫助執行緒傳遞執行緒關心的時間狀態。
關於等待/通知,要記住的關鍵點是:
必須從同步環境內呼叫wait()、notify()、notifyAll()方法。執行緒不能呼叫物件上等待或通知的方法,除非它擁有那個物件的鎖。
wait()、notify()、notifyAll()都是Object的例項方法。與每個物件具有鎖一樣,每個物件可以有一個執行緒列表,他們等待來自該訊號(通知)。執行緒通過執行物件上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到呼叫物件的notify()方法為止。如果多個執行緒在同一個物件上等待,則將只選擇一個執行緒(不保證以何種順序)繼續執行。如果沒有執行緒等待,則不採取任何特殊操作。
下面看個例子就明白了:
/** * 計算輸出其他執行緒鎖計算的資料 */ public class ThreadA { public static void main(String[] args) { ThreadB b=new ThreadB(); //啟動計算執行緒 b.start(); //執行緒A擁有b物件上的鎖。執行緒為了呼叫wait()或notify()方法,該執行緒必須是那個物件鎖的擁有者 synchronized (b) { try { System.out.println("等待物件b完成計算......"); b.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("b物件計算的總和是:" + b.total); } } } /** * 計算1+2+3+...+100的和 */ public class ThreadB extends Thread { int total; public void run(){ synchronized (this) { for (int i=0;i<101;i++){ total+=i; } //(完成計算了)喚醒在此物件監視器上等待的單個執行緒,在本例中執行緒A被喚醒 notify(); } } }
執行結果:
等待物件b完成計算......
b物件計算的總和是:5050
千萬注意:
當在物件上呼叫wait()方法時,執行該程式碼的執行緒立即放棄它在物件上的鎖。然而呼叫notify()時,並不意味著這時執行緒會放棄其鎖。如果執行緒榮然在完成同步程式碼,則執行緒在移出之前不會放棄鎖。因此,只要呼叫notify()並不意味著這時該鎖變得可用。
二、多個執行緒在等待一個物件鎖時候使用notifyAll()
在多數情況下,最好通知等待某個物件的所有執行緒。如果這樣做,可以在物件上使用notifyAll()讓所有在此物件上等待的執行緒衝出等待區,返回到可執行狀態。
舉個例子:
/**
* 計算執行緒
*/
public class Calculator extends Thread {
int total;
@Override
public void run() {
synchronized (this) {
for(int i=0;i<101;i++){
total+=i;
}
}
//通知所有在此物件上等待的執行緒
notifyAll();
}
}
/**
* 獲取計算結果並輸出
*/
public class ReaderResult extends Thread {
Calculator c;
public ReaderResult(Calculator c) {
this.c = c;
}
public void run(){
synchronized (c) {
try {
System.out.println(Thread.currentThread() + "等待計算結果......");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+ "計算結果為:" + c.total);
}
}
public static void main(String[] args) {
Calculator calculator=new Calculator();
//啟動三個執行緒,分別獲取計算結果
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
//啟動計算執行緒
calculator.start();
}
}
執行結果:
Thread[Thread-1,5,main]等待計算結果......
Thread[Thread-2,5,main]等待計算結果......
Thread[Thread-3,5,main]等待計算結果......
Exception in thread"Thread-0" java.lang.IllegalMonitorStateException
atjava.lang.Object.notifyAll(Native Method)
attest.Calculator.run(Calculator.java:15)
Thread[Thread-3,5,main]計算結果為:5050
Thread[Thread-2,5,main]計算結果為:5050
Thread[Thread-1,5,main]計算結果為:5050
執行結果表明,程式中有異常,並且多次執行結果可能有多種輸出結果。這就是說明,這個多執行緒的互動程式還存在問題。究竟是出了什麼問題,需要深入的分析和思考,下面將做具體分析。
實際上,上面這個程式碼中,我們期望的是讀取結果的執行緒在計算執行緒呼叫notifyAll()之前等待即可。但是,如果計算執行緒先執行,並在讀取結果執行緒等待之前呼叫了notify()方法,那麼又會發生什麼呢?這種情況是可能發生的。因為無法保證執行緒的不同部分將按照什麼順序來執行。幸運的是當讀取執行緒執行時,它只能馬上進入等待狀態----它沒有做任何事情來檢查等待的事件是否已經發生。 ----因此,如果計算執行緒已經呼叫了notifyAll()方法,那麼它就不會再次呼叫notifyAll(),----並且等待的讀取執行緒將永遠保持等待。這當然是開發者所不願意看到的問題。
因此,當等待的事件發生時,需要能夠檢查notifyAll()通知事件是否已經發生。
通常,解決上面問題的最佳方式是利用某種迴圈,該迴圈檢查某個條件表示式,只有當正在等待的事情還沒有發生的情況下,它才繼續等待。