中斷JAVA執行緒

一任天然發表於2008-09-10

      在JAVA中,通過其對執行緒類的內嵌支援,程式設計人員編寫多執行緒程式是很簡易的。然而,在程式設計人員面前,多執行緒呈現出了一組新的難題,如果沒有被恰當的解決,將導致意外的行為以及細微的、難以發現的錯誤。
      在本篇文章中,我們針對這些難題之一:如何中斷一個正在執行的執行緒。
                                                                                   
背景
    中斷(Interrupt)一個執行緒意味著在該執行緒完成任務之前停止其正在進行的一切,有效地中止其當前的操作。執行緒是死亡、還是等待新的任務或是繼續執行至下一步,就取決於這個程式。 雖然初次看來它可能顯得簡單,但是,你必須進行一些預警以實現期望的結果。你最好還是牢記以下的幾點告誡。

    首先,忘掉Thread.stop方法。雖然它確實停止了一個正在執行的執行緒,然而,這種方法是不安全也是不受提倡的,這意味著,在未來的JAVA版本中,它將不復存在。

    一些輕率的傢伙可能被另一種方法Thread.interrupt所迷惑。儘管,其名稱似乎在暗示著什麼,然而,這種方法並不會中斷一個正在執行的執行緒(待會將進一步說明),正如Listing A中描述的那樣。它建立了一個執行緒,並且試圖使用Thread.interrupt方法停止該執行緒。Thread.sleep()方法的呼叫,為執行緒的初始化和中止提供了充裕的時間。執行緒本身並不參與任何有用的操作。

Listing A


如果你執行了Listing A中的程式碼,你將在控制檯看到以下輸出:

Starting thread...

Thread is running...

Thread is running...

Thread is running...

Interrupting thread...

Thread is running...

Thread is running...

Thread is running...

Stopping application...

Thread is running...

Thread is running...

Thread is running...
...............................
甚至,在Thread.interrupt()被呼叫後,執行緒仍然繼續執行。

真正地中斷一個執行緒

    中斷執行緒最好的,最受推薦的方式是,使用共享變數(shared variable)發出訊號,告訴執行緒必須停止正在執行的任務。執行緒必須週期性的核查這一變數(尤其在冗餘操作期間),然後有秩序地中止任務。Listing B描述了這一方式。

Listing B

  1. class  Example2  extends  Thread {
  2.      volatile   boolean  stop =  false ;
  3.      public   static   void  main(String args[])  throws  Exception {
  4.         Example2 thread =  new  Example2();
  5.         System.out.println( "Starting thread..." );
  6.         thread.start();
  7.         Thread.sleep( 3000 );
  8.         System.out.println( "Asking thread to stop..." );
  9.         thread.stop =  true ;
  10.         Thread.sleep( 3000 );
  11.         System.out.println( "Stopping application..." );
  12.          // System.exit( 0 );
  13.     }
  14.      public   void  run() {
  15.          while  (!stop) {
  16.             System.out.println( "Thread is running..." );
  17.              long  time = System.currentTimeMillis();
  18.              while  ((System.currentTimeMillis() - time <  1000 ) && (!stop)) {
  19.             }
  20.         }
  21.         System.out.println( "Thread exiting under request..." );
  22.     }
  23. }

執行Listing B中的程式碼將產生如下輸出(注意執行緒是如何有秩序的退出的)

Starting thread...

Thread is running...

Thread is running...

Thread is running...

Asking thread to stop...

Thread exiting under request...

Stopping application...

   雖然該方法要求一些編碼,但並不難實現。同時,它給予執行緒機會進行必要的清理工作,這在任何一個多執行緒應用程式中都是絕對需要的。請確認將共享變數定義成volatile 型別或將對它的一切訪問封入同步的塊/方法(synchronized blocks/methods)中。

到目前為止一切順利!但是,當執行緒等待某些事件發生而被阻塞,又會發生什麼?當然,如果執行緒被阻塞,它便不能核查共享變數,也就不能停止。這在許多情況下會發生,例如呼叫Object.wait()、ServerSocket.accept()和DatagramSocket.receive()時,這裡僅舉出一些。

他們都可能永久的阻塞執行緒。即使發生超時,在超時期滿之前持續等待也是不可行和不適當的,所以,要使用某種機制使得執行緒更早地退出被阻塞的狀態。

很不幸運,不存在這樣一種機制對所有的情況都適用,但是,根據情況不同卻可以使用特定的技術。在下面的環節,我將解答一下最普遍的例子。

使用Thread.interrupt()中斷執行緒

  正如Listing A中所描述的,Thread.interrupt()方法不會中斷一個正在執行的執行緒。這一方法實際上完成的是,線上程受到阻塞時丟擲一箇中斷訊號,這樣執行緒就得以退出阻塞的狀態。更確切的說,如果執行緒被Object.wait, Thread.join和Thread.sleep三種方法之一阻塞,那麼,它將接收到一箇中斷異常(InterruptedException),從而提早地終結被阻塞狀態。

    因此,如果執行緒被上述幾種方法阻塞,正確的停止執行緒方式是設定共享變數,並呼叫interrupt()(注意變數應該先設定)。如果執行緒沒有被阻塞,這時呼叫interrupt()將不起作用;否則,執行緒就將得到異常(該執行緒必須事先預備好處理此狀況),接著逃離阻塞狀態。在任何一種情況中,最後執行緒都將檢查共享變數然後再停止。Listing C這個示例描述了該技術。

Listing C

  1. class  Example3  extends  Thread {
  2.      volatile   boolean  stop =  false ;
  3.      public   static   void  main(String args[])  throws  Exception {
  4.         Example3 thread =  new  Example3();
  5.         System.out.println( "Starting thread..." );
  6.         thread.start();
  7.         Thread.sleep( 3000 );
  8.         System.out.println( "Asking thread to stop..." );
  9.         thread.stop =  true ; // 如果執行緒阻塞,將不會檢查此變數
  10.         thread.interrupt();
  11.         Thread.sleep( 3000 );
  12.         System.out.println( "Stopping application..." );
  13.          // System.exit( 0 );
  14.     }
  15.      public   void  run() {
  16.          while  (!stop) {
  17.             System.out.println( "Thread running..." );
  18.              try  {
  19.                 Thread.sleep( 1000 );
  20.             }  catch  (InterruptedException e) {
  21.                 System.out.println( "Thread interrupted..." );
  22.             }
  23.         }
  24.         System.out.println( "Thread exiting under request..." );
  25.     }
  26. }

一旦Listing C中的Thread.interrupt()被呼叫,執行緒便收到一個異常,於是逃離了阻塞狀態並確定應該停止。執行以上程式碼將得到下面的輸出:

Starting thread...

Thread running...

Thread running...

Thread running...

Asking thread to stop...

Thread interrupted...

Thread exiting under request...

Stopping application...


中斷I/O操作
    然而,如果執行緒在I/O操作進行時被阻塞,又會如何?I/O操作可以阻塞執行緒一段相當長的時間,特別是牽扯到網路應用時。例如,伺服器可能需要等待一個請求(request),又或者,一個網路應用程式可能要等待遠端主機的響應。

如果你正使用通道(channels)(這是在Java 1.4中引入的新的I/O API),那麼被阻塞的執行緒將收到一個ClosedByInterruptException異常。如果情況是這樣,其程式碼的邏輯和第三個例子中的是一樣的,只是異常不同而已。

但是,你可能正使用Java1.0之前就存在的傳統的I/O,而且要求更多的工作。既然這樣,Thread.interrupt()將不起作用,因為執行緒將不會退出被阻塞狀態。Listing D描述了這一行為。儘管interrupt()被呼叫,執行緒也不會退出被阻塞狀態

Listing D

  1. import  java.io.*;
  2. import  java.net.ServerSocket;
  3. import  java.net.Socket;
  4. class  Example4  extends  Thread {
  5.      public   static   void  main(String args[])  throws  Exception {
  6.         Example4 thread =  new  Example4();
  7.         System.out.println( "Starting thread..." );
  8.         thread.start();
  9.         Thread.sleep( 3000 );
  10.         System.out.println( "Interrupting thread..." );
  11.         thread.interrupt();
  12.         Thread.sleep( 3000 );
  13.         System.out.println( "Stopping application..." );
  14.          // System.exit( 0 );
  15.     }
  16.      public   void  run() {
  17.         ServerSocket socket;
  18.          try  {
  19.             socket =  new  ServerSocket( 7856 );
  20.         }  catch  (IOException e) {
  21.             System.out.println( "Could not create the socket..." );
  22.              return ;
  23.         }
  24.          while  ( true ) {
  25.             System.out.println( "Waiting for connection..." );
  26.              try  {
  27.                 Socket sock = socket.accept();
  28.             }  catch  (IOException e) {
  29.                 System.out.println( "accept() failed or interrupted..." );
  30.             }
  31.         }
  32.     }
  33. }

   很幸運,Java平臺為這種情形提供了一項解決方案,即呼叫阻塞該執行緒的套接字的close()方法。在這種情形下,如果執行緒被I/O操作阻塞,該執行緒將接收到一個SocketException異常,這與使用interrupt()方法引起一個InterruptedException異常被丟擲非常相似。

唯一要說明的是,必須存在socket的引用(reference),只有這樣close()方法才能被呼叫。這意味著socket物件必須被共享。Listing E描述了這一情形。執行邏輯和以前的示例是相同的。

Listing E

  1. import  java.net.*;
  2. import  java.io.*;
  3. class  Example5  extends  Thread {
  4.      volatile   boolean  stop =  false ;
  5.      volatile  ServerSocket socket;
  6.      public   static   void  main(String args[])  throws  Exception {
  7.         Example5 thread =  new  Example5();
  8.         System.out.println( "Starting thread..." );
  9.         thread.start();
  10.         Thread.sleep( 3000 );
  11.         System.out.println( "Asking thread to stop..." );
  12.         thread.stop =  true ;
  13.         thread.socket.close();
  14.         Thread.sleep( 3000 );
  15.         System.out.println( "Stopping application..." );
  16.          // System.exit( 0 );
  17.     }
  18.      public   void  run() {
  19.          try  {
  20.             socket =  new  ServerSocket( 7856 );
  21.         }  catch  (IOException e) {
  22.             System.out.println( "Could not create the socket..." );
  23.              return ;
  24.         }
  25.          while  (!stop) {
  26.             System.out.println( "Waiting for connection..." );
  27.              try  {
  28.                 Socket sock = socket.accept();
  29.             }  catch  (IOException e) {
  30.                 System.out.println( "accept() failed or interrupted..." );
  31.             }
  32.         }
  33.         System.out.println( "Thread exiting under request..." );
  34.     }
  35. }

以下是執行Listing E中程式碼後的輸出:

Starting thread...

Waiting for connection...

Asking thread to stop...

accept() failed or interrupted...

Thread exiting under request...

Stopping application...

多執行緒是一個強大的工具,然而它正呈現出一系列難題。其中之一是如何中斷一個正在執行的執行緒。如果恰當地實現,使用上述技術中斷執行緒將比使用Java平臺上已經提供的內嵌操作更為簡單。

 

相關文章