Java多執行緒學習筆記(二) (轉)

amyz發表於2007-11-07
Java多執行緒學習筆記(二) (轉)[@more@]

多執行緒學習筆記(二)

作者:陶冶(無邪)
時間:.6.21

四、Java的等待通知機制
  在有些時候,我們需要在幾個或多個執行緒中按照一定的秩序來共享一定的資源。例如生產者--消費者的關係,在這一對關係中實際情況總是先有生產者生產了產品後,消費者才有可能消費;又如在父--子關係中,總是先有父親,然後才能有兒子。然而在沒有引入等待通知機制前,我們得到的情況卻常常是錯誤的。這裡我引入《用執行緒獲得強大的功能》一文中的生產者--消費者的例子:
/* ==================================================================================
 * :ThreadDemo07.java
 * 描述:生產者--消費者
 * 注:其中的一些註釋是我根據自己的理解加註的
 * ==================================================================================
 */

// 共享的資料
 class ShareData{
 private char c;
 
 public void setShareChar(char c){
 this.c = c;
 }
 
 public char getShareChar(){
 return this.c;
 }
 }
 
 // 生產者執行緒
 class Producer extends Thread{
 
 private ShareData s;
 
 Producer(ShareData s){
 this.s = s;
 }
 
 public void run(){
 for (char ch = 'A'; ch <= 'Z'; ch++){
 try{
 Thread.sleep((int)Math.ran() * 4000);
 }catch(InterruptedException e){}
 
 // 生產
 s.setShareChar(ch);
 System.out.println(ch + " producer by producer.");
 }
 }
 }
 
 // 消費者執行緒
 class Consumer extends Thread{
 
 private ShareData s;
 
 Consumer(ShareData s){
 this.s = s;
 }
 
 public void run(){
 char ch;
 
 do{
 try{
 Thread.sleep((int)Math.random() * 4000);
 }catch(InterruptedException e){}
 // 消費
 ch = s.getShareChar();
 System.out.println(ch + " consumer by consumer.");
 }while(ch != 'Z');
 }
 }

class Test{
 public static void main(String argv[]){
 ShareData s = new ShareData();
 new Consumer(s).start();
 new Producer(s).start();
 }
}

  在以上的中,模擬了生產者和消費者的關係,生產者在一個迴圈中不斷生產了從A-Z的共享資料,而消費者則不斷地消費生產者生產的A-Z的共享資料。我們開始已經說過,在這一對關係中,必須先有生產者生產,才能有消費者消費。但如果執行我們上面這個程式,結果卻出現了在生產者沒有生產之前,消費都就已經開始消費了或者是生產者生產了卻未能被消費者消費這種反常現象。為了解決這一問題,引入了等待通知(wait/notify)機制如下:
  1、在生產者沒有生產之前,通知消費者等待;在生產者生產之後,馬上通知消費者消費。
  2、在消費者消費了之後,通知生產者已經消費完,需要生產。
下面修改以上的例子(源自《用執行緒獲得強大的功能》一文):

/* ==================================================================================
 * 檔案:ThreadDemo08.java
 * 描述:生產者--消費者
 * 注:其中的一些註釋是我根據自己的理解加註的
 * ==================================================================================
 */

class ShareData{
 
 private char c;
 // 通知變數
 private boolean writeable = true;

 // ------------------------------------------------------------------------- 
 // 需要注意的是:在wait()方法時,需要把它放到一個同步段裡,否則將會出現
 // "java.lang.IllegalMonitorStateException: current thread not owner"的異常。
 // -------------------------------------------------------------------------
 public synchronized void setShareChar(char c){
 if (!writeable){
 try{
 // 未消費等待
 wait();
 }catch(InterruptedException e){}
 }
 
 this.c = c;
 // 標記已經生產
 writeable = false;
 // 通知消費者已經生產,可以消費
 notify();
 }
 
 public synchronized char getShareChar(){
 if (writeable){
 try{
 // 未生產等待
 wait();
 }catch(InterruptedException e){} 
 }
 // 標記已經消費
 writeable = true;
 // 通知需要生產
 notify();
 return this.c;
 }
}

// 生產者執行緒
class Producer extends Thread{
 
 private ShareData s;
 
 Producer(ShareData s){
 this.s = s;
 }
 
 public void run(){
 for (char ch = 'A'; ch <= 'Z'; ch++){
 try{
 Thread.sleep((int)Math.random() * 400);
 }catch(InterruptedException e){}
 
 s.setShareChar(ch);
 System.out.println(ch + " producer by producer.");
 }
 }
}

// 消費者執行緒
class Consumer extends Thread{
 
 private ShareData s;
 
 Consumer(ShareData s){
 this.s = s;
 }
 
 public void run(){
 char ch;
 
 do{
 try{
 Thread.sleep((int)Math.random() * 400);
 }catch(InterruptedException e){}
 
 ch = s.getShareChar();
 System.out.println(ch + " consumer by consumer.**");
 }while (ch != 'Z');
 }
}

class Test{
 public static void main(String argv[]){
 ShareData s = new ShareData();
 new Consumer(s).start();
 new Producer(s).start();
 }
}

  在以上程式中,設定了一個通知變數,每次在生產者生產和消費者消費之前,都測試通知變數,檢查是否可以生產或消費。最開始設定通知變數為true,表示還未生產,在這時候,消費者需要消費,於時修改了通知變數,呼叫notify()發出通知。這時由於生產者得到通知,生產出第一個產品,修改通知變數,向消費者發出通知。這時如果生產者想要繼續生產,但因為檢測到通知變數為false,得知消費者還沒有生產,所以呼叫wait()進入等待狀態。因此,最後的結果,是生產者每生產一個,就通知消費者消費一個;消費者每消費一個,就通知生產者生產一個,所以不會出現未生產就消費或生產過剩的情況。

五、執行緒的中斷
  在很多時候,我們需要在一個執行緒中調控另一個執行緒,這時我們就要用到執行緒的中斷。用最簡單的話也許可以說它就相當於機中的暫停一樣,當第一次按下暫停時,停止播放,再一次按下暫停時,繼續從剛才暫停的地方開始重新播放。而在Java中,這個暫停按鈕就是Interrupt()方法。在第一次呼叫interrupt()方法時,執行緒中斷;當再一次呼叫interrupt()方法時,執行緒繼續執行直到終止。這裡依然引用《用執行緒獲得強大功能》一文中的程式片斷,但為了更方便看到中斷的過程,我在原程式的基礎上作了些改進,程式如下:

/* ===================================================================================
 * 檔案:ThreadDemo09.java
 * 描述:執行緒的中斷
 * ===================================================================================
 */
class ThreadA extends Thread{
 
 private Thread thdOther;
 
 ThreadA(Thread thdOther){
 this.thdOther = thdOther;
 }
 
 public void run(){
 
 System.out.println(getName() + " 執行...");
 
 int sleepTime = (int)(Math.random() * 10000);
 System.out.println(getName() + " 睡眠 " + sleepTime
 + " 毫秒。");
 
 try{
 Thread.sleep(sleepTime);
 }catch(InterruptedException e){}
 
 System.out.println(getName() + " 覺醒,即將中斷執行緒 B。");
 // 中斷執行緒B,執行緒B暫停執行
 thdOther.interrupt();
 }
}

class Threa extends Thread{
 int count = 0;
 
 public void run(){
 
 System.out.println(getName() + " 執行...");
 
 while (!this.isInterrupted()){
 System.out.println(getName() + " 執行中 " + count++);
 
 try{ 
 Thread.sleep(10);
 }catch(InterruptedException e){
 int sleepTime = (int)(Math.random() * 10000);
 System.out.println(getName() + " 睡眠" + sleepTime
 + " 毫秒。覺醒後立即執行直到終止。");
 
 try{
 Thread.sleep(sleepTime);
 }catch(InterruptedException m){}
 
 System.out.println(getName() + " 已經覺醒,執行終止...");
 // 重新設定標記,繼續執行 
 this.interrupt();
 }
 }
 
 System.out.println(getName() + " 終止。"); 
 }
}

class Test{
 public static void main(String argv[]){
 ThreadB thdb = new ThreadB();
 thdb.setName("ThreadB");
 
 ThreadA thda = new ThreadA(thdb);
 thda.setName("ThreadA");
 
 thdb.start();
 thda.start();
 }
}

  執行以上程式,你可以清楚地看到中斷的過程。首先執行緒B開始執行,接著執行執行緒A,線上程A睡眠一段時間覺醒後,呼叫interrupt()方法中斷執行緒B,此是可能B正在睡眠,覺醒後掏出一個InterruptedException異常,其中的語句,為了更清楚地看到執行緒的中斷恢復,我在InterruptedException異常後增加了一次睡眠,當睡眠結束後,執行緒B呼叫自身的interrupt()方法恢復中斷,這時測試isInterrupt()返回true,執行緒退出。

更多的執行緒組及相關的更詳細的資訊,請參考《用執行緒獲得強大功能》一文及《Thinking in Java》。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-981191/,如需轉載,請註明出處,否則將追究法律責任。

相關文章