1. 程式人生 > >java中,如何安全的結束一個正在執行的執行緒?

java中,如何安全的結束一個正在執行的執行緒?

如何等待一個執行緒結束。那麼如果不希望等待執行緒結束,而是根據問題的需要隨時都要中斷執行緒使其結束,這種對執行緒的控制方法該如何實現呢?
  解決思路
  首先必須先明確“中斷”這個概念的實際含義,這裡的中斷是指一個執行緒在其任務完成之前被強行停止,提前消亡的過程。查閱JDK的幫助文件,可以找到這樣一個和中斷有關的方法:interrupt()。
  它的語法格式如下所示:
  public void interrupt()
  該方法的功能是中斷一個執行緒的執行。但是,在實際使用當中發現,這個方法不一定能夠真地中斷一個正在執行的執行緒。下面通過一個例子來看一看使用interrput()方法中斷一個執行緒時所出現的結果。程式程式碼如下所示:
  // 例4.4.1 InterruptThreadDemo.java
  class MyThread extends Thread
  {
  public void run()
  {
  while(true) // 無限迴圈,並使執行緒每隔1秒輸出一次字串
  { 
  System.out.println(getName()+' is running'); 
  try{ 
  sleep(1000);
  }catch(InterruptedException e){
  System.out.println(e.getMessage());
  }
  }
  }
  }
  class InterruptThreadDemo
  {
  public static void main(String[] args) throws InterruptedException
  {
  MyThread m=new MyThread(); // 建立執行緒物件m
  System.out.println('Starting thread...');
  m.start(); // 啟動執行緒m
  Thread.sleep(2000); //主執行緒休眠2秒,使執行緒m一直得到執行
  System.out.println('Interrupt thread...');
  m.interrupt(); // 呼叫interrupt()方法中斷執行緒m
  Thread.sleep(2000); // 主執行緒休眠2秒,觀察中斷後的結果
  System.out.println('Stopping application...'); // 主執行緒結束
  }
  }
  這個程式的本意是希望,當程式執行到m.interrupt()方法後,執行緒m將被中斷並進入消亡狀態。然而執行這個程式,螢幕裡顯示了出人意料的結果,如圖4.4.1所示。
  通過對結果的分析,可以發現,使用者執行緒在呼叫了interrupt()方法之後並沒有被中斷,而是繼續執行,直到人為地按下Ctrl+C或者Pause鍵為止。這個例子說明一個事實,直接使用interrput()方法並不能中斷一個正在執行的執行緒。那麼用什麼樣的方法才能中斷一個正在執行的執行緒呢?
  
  圖 4.4.1 對執行緒呼叫了interrupt()
  通過查閱JDK,有些讀者可能會看到Thread類中所提供的stop()方法。但是在這裡需要強調的是,雖然該方法確實能夠停止一個正在執行的執行緒,但是該方法是不安全的,因為有時使用它會導致嚴重的系統錯誤。例如一個執行緒正在等待關鍵的資料結構,並只完成了部分地改變,如果在這一時刻停止該執行緒,那麼資料結構將會停留在錯誤的狀態上。正因為如此,在Java後期的版本中,它將不復存在。因此,使用stop()方法來中斷一個執行緒是不合適的。
  這時我們想到了使用共享變數的方式,通過一個共享訊號變數來通知執行緒是否需要中斷,如果需要中斷,則停止正在執行的任務,否則讓任務繼續執行。這種方式是如何實現的呢?
  具體步驟
  在這種方式中,之所以引入共享變數,是因為該變數可以被多個執行相同任務的執行緒用來作為是否中斷的訊號,通知中斷執行緒的執行。下面通過在程式中引入共享變數來改進前面例4.4.1,改進後的程式碼如下所示:
  // 例4.4.2 InterruptThreadDemo2.java
  class MyThread extends Thread
  {
  boolean stop = false; // 引入一個布林型的共享變數stop
  public void run()
  {
  while(!stop) // 通過判斷stop變數的值來確定是否繼續執行執行緒體
  {
  System.out.println(getName()+' is running');
  try
  {
  sleep(1000); 
  }catch(InterruptedException e){
  System.out.println(e.getMessage());
  }
  }
  System.out.println('Thread is exiting...');
  }
  }
  class InterruptThreadDemo2
  {
  public static void main(String[] args) throws InterruptedException
  {
  MyThread m=new MyThread();
  System.out.println('Starting thread...');
  m.start();
  Thread.sleep(3000); 
  System.out.println('Interrupt thread...');
  m.stop=true; // 修改共享變數
  Thread.sleep(3000); // 主執行緒休眠以觀察執行緒中斷後的情況
  System.out.println('Stopping application...');
  }
  }
  在使用共享變數來中斷一個執行緒的過程中,執行緒體通過迴圈來週期性的檢查這一變數的狀態。如果變數的狀態改變,說明程式發出了立即中斷該執行緒的請求,此時,迴圈體條件不再滿足,結束迴圈,進而結束執行緒的任務。程式執行的結果如圖4.4.2所示:
  
  圖4.4.2 引入共享變數來中斷執行緒
  其中,主程式中的第二個Thread.sleep(3000);語句就是用來使程式不提早結束,以便觀察執行緒m的中斷情況。結果是一旦將共享變數stop設定為true,則中斷立即發生。
  為了更加安全起見,通常需要將共享變數定義為volatile型別或者將對該共享變數的一切訪問封裝到同步的程式碼或者同步方法中去。後者所提到的技術將在第4.5節中介紹。
  在多執行緒的程式中,當出現有兩個或多個執行緒共享同一例項變數的情況時,每一個執行緒可以保持這個例項變數自己的私有副本,變數的實際備份在不同時間被更新。而問題就是變數的主備份總是需要反映它的當前狀態,此時反而使效率降低。為保證效率,只需要簡單地指定變數為volatile型別即可,它可以告訴編譯器必須總是使用volatile變數的主備份(或者至少總是保持任何私有的備份和最新的備份一樣,反之亦然)。同樣,對主變數的訪問必須同任何私有備份一樣,精確地順序執行。
  如果需要一次中斷所有由同一執行緒類建立的執行緒,該怎樣實現呢?有些讀者可能馬上就想到了對每一個執行緒物件通過設定共享變數的方式來中斷執行緒。這種方法當然可以,那麼有沒有更好的方法呢?
  此時只需將共享變數設定為static型別的即可。然後在主程式中當需要中斷所有同一個執行緒類建立的執行緒物件時,使用MyThread.stop=true;語句就可實現對所有同一個執行緒類建立的執行緒物件的中斷操作,而且效率明顯提高。讀者不妨試一試。
  專家說明
  通過本節介紹瞭如何中斷一個正在執行的執行緒,既不是用stop()方法,也不是用interrupt()方法,而是通過引入了共享變數的形式有效地解決了執行緒中斷的問題。其實這種方法有很多好處,它避免了一些無法想象的意外情況的發生,特別是將共享變數所訪問的一切程式碼都封裝到同步方法中以後,安全性將更高。在本節中,還可以嘗試建立多個執行緒來檢驗這種中斷方式的好處。此外,還介紹了volatile型別說明符的作用,這更加有助於提高中斷執行緒的效率,值得提倡。
  專家指點
  本小節不僅要掌握如何使用共享變數的方法來中斷一個執行緒,還要明白為什麼使用其他方法來中斷執行緒就不安全。其實,在多執行緒的排程當中還會出現一個問題,那就是死鎖。死鎖的出現將導致執行緒間均無法向前推進,從而陷入尷尬的局面。因此,為減少出現死鎖的發生,Java 1.2以後的版本中已經不再使用Thread類的stop(),suspend(),resume()以及destroy()方法。特別是不安全的stop()方法,原因就是它會解除由執行緒獲取的所有鎖定,而且一旦物件處於一種不連貫的狀態,那麼其他執行緒就能在那種狀態下檢查和修改它們,結果導致很難再檢查出問題的真正所在。因此最好的方法就是,用一個標誌來告訴執行緒什麼時候應該退出自己的run()方法,並中斷自己的執行。通過後面小節的學習將會更好的理解這個問題。
  相關問題
  如果一個執行緒由於等待某些事件的發生而被阻塞,又該如何實現該執行緒的中斷呢?比如當一個執行緒由於需要等候鍵盤輸入而被阻塞,處於不可執行狀態時,即使主程式中將該執行緒的共享變數設定為true,但該執行緒此時根本無法檢查迴圈標誌,當然也就無法立即中斷。
  其實,這種情況經常會發生,比如呼叫Thread.join()方法,或者Thread.sleep()方法,在網路中呼叫ServerSocket.accept()方法,或者呼叫了DatagramSocket.receive()方法時,都有可能導致執行緒阻塞。即便這樣,仍然不要使用stop()方法,而是使用Thread提供的interrupt()方法,因為該方法雖然不會中斷一個正在執行的執行緒,但是它可以使一個被阻塞的執行緒丟擲一箇中斷異常,從而使執行緒提前結束阻塞狀態,退出堵塞程式碼。
  下面看一個例子來說明這個問題:
  // 例4.4.3 InterruptThreadDemo3.java
  class MyThread extends Thread
  {
  volatile boolean stop = false;
  public void run()
  {
  while(!stop)
  {
  System.out.println(getName()+' is running');
  try
  {
  sleep(1000);
  }catch(InterruptedException e){ 
  System.out.println('week up from blcok...');
  stop=true; // 在異常處理程式碼中修改共享變數的狀態
  }
  }
  System.out.println(getName()+' is exiting...');
  }
  }
  class InterruptThreadDemo3
  {
  public static void main(String[] args) throws InterruptedException
  {
  MyThread m1=new MyThread();
  System.out.println('Starting thread...');
  m1.start();
  Thread.sleep(3000); 
  System.out.println('Interrupt thread...:'+m1.getName());
  m1.stop=true; // 設定共享變數為true
  m1.interrupt(); // 阻塞時退出阻塞狀態
  Thread.sleep(3000); // 主執行緒休眠3秒以便觀察執行緒m1的中斷情況
  System.out.println('Stopping application...');
  }
  }
  程式中如果執行緒m1發生了阻塞,那麼雖然執行了m1.stop=true;語句,但是stop的值並未改變。為了能夠中斷該執行緒,必須在異常處理語句中對共享變數的值進行重新設定,從而實現了在任何情況下都能夠中斷執行緒的目的。
  一定要記住,m1.interrupt();語句只有當執行緒發生阻塞時才有效。它的作用就是丟擲一個InterruptedException類的異常物件,使try…catch語句捕獲異常,並對其進行處理。請讀者仔細研究這個程式,以便能夠看出其中的巧妙之處。