1. 程式人生 > >Java執行緒詳解(6)-執行緒的互動

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()通知事件是否已經發生。

        通常,解決上面問題的最佳方式是利用某種迴圈,該迴圈檢查某個條件表示式,只有當正在等待的事情還沒有發生的情況下,它才繼續等待。