【java併發編程】interrupt()陷阱和線程正確的終止方式

一、背景

 在學習Java多線程知識時,掌握線程的啓動和終止是應用基礎。但由於多線程的複雜性,導致簡單的線程終止程序出現意外的行爲以及細微的、難以發現的錯誤。

  首先還是大概的羅列下停止線程的方法:

1、使用stop()方法:由於安全問題,已經不再被推薦使用,和suspend、resume一樣。

2、使用退出標誌位終止線程:引入一個共享變量,volatile類型或者使用synchronized來監視共享變量相關操作的方法,然後在run()方法中,通過while循環不停的輪詢這個標誌。

3、使用Interrupt()方法中斷線程:中斷狀態是線程的一個標識位,而中斷操作是一種簡便的線程間交互方式,而這種交互方式最適合用來取消或停止任務。

      那麼,通常情況下程序員將使用2和3兩種方式來終止線程,這也是《java併發編程藝術》一書中所推薦的方式。但是對於剛接觸多線程的同學來說,還是很容易被Thread.interrupt所迷惑。儘管,其名稱似乎在暗示着什麼,然而,這種方法並不會中斷一個正在運行的線程 。

例如下面代碼所描述的情況,它創建了一個線程,並且試圖使用Thread.interrupt方法停止該線程。Thread.sleep()方法的調用,爲線程的初 始化和中止提供了充裕的時間。線程本身並不參與任何有用的操作。 

class Example extends Thread {  
      //退出標誌位
      private boolean stop=false;  
      
      public static void main( String args[] ) throws Exception {  
            Example thread = new Example1();  
            System.out.println( "Starting thread..." );  
            thread.start();  
            Thread.sleep( 3000 );  

            System.out.println( "Interrupting thread..." );  
            thread.interrupt();  

            Thread.sleep( 3000 );  
            System.out.println("Stopping application..." );  
            
      }  

      public void run() {  
            while(!stop){  
                System.out.println( "Thread is running..." );  
                long time = System.currentTimeMillis();  
                while((System.currentTimeMillis()-time < 1000)) {  
                }  
            }  
            System.out.println("Thread exiting under request..." );  
      }  
}  

控制檯看到以下輸出: 

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.interrrupt()這種方法並沒有中斷正在運行的線程 。

接下來,我將通過對Thread.interrupt()的詳解,來探討怎麼讓終止線程的做法顯得更加安全和優雅。


二、interrupt()詳解

2.1 interrupt JDK文檔

關於interrupt(),java的djk文檔描述如下:http://docs.oracle.com/javase/7/docs/api/

Interrupts this thread.
Unless the current thread is interrupting itself, which is always permitted, the checkAccess method of this thread is invoked, which may cause a SecurityException to be thrown.

If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.

If this thread is blocked in an I/O operation upon an interruptible channel then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a ClosedByInterruptException.

If this thread is blocked in a Selector then the thread's interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector's wakeup method were invoked.

If none of the previous conditions hold then this thread's interrupt status will be set.

Interrupting a thread that is not alive need not have any effect.

中文翻譯

interrupt()的作用是中斷本線程。
本線程中斷自己是被允許的;其它線程調用本線程的interrupt()方法時,會通過checkAccess()檢查權限。這有可能拋出SecurityException異常。
如果本線程是處於阻塞狀態:調用線程的wait(), wait(long)或wait(long, int)會讓它進入等待(阻塞)狀態,或者調用線程的join(), join(long), join(long, int), sleep(long), sleep(long, int)也會讓它進入阻塞狀態。若線程在阻塞狀態時,調用了它的interrupt()方法,那麼它的“中斷狀態”會被清除並且會收到一個InterruptedException異常。例如,線程通過wait()進入阻塞狀態,此時通過interrupt()中斷該線程;調用interrupt()會立即將線程的中斷標記設爲“true”,但是由於線程處於阻塞狀態,所以該“中斷標記”會立即被清除爲“false”,同時,會產生一個InterruptedException的異常。
如果線程被阻塞在一個Selector選擇器中,那麼通過interrupt()中斷它時;線程的中斷標記會被設置爲true,並且它會立即從選擇操作中返回。
如果不屬於前面所說的情況,那麼通過interrupt()中斷線程時,它的中斷標記會被設置爲“true”。
中斷一個“已終止的線程”不會產生任何操作。

白話文

調用一個線程的Interrupt方法會把線程的狀態改爲中斷態。這其中又可以細分成兩個方面:

1) 對於因執行了sleep、wait、join方法而阻塞的線程:調用Interrupt方法會使他們不再阻塞,同時會拋出 InterruptedException異常。並且中斷標誌被清除,重新設置爲false。比如一個線程A正在sleep中,這時候另外一個程序裏去調用A的interrupt方法,這時就會迫使A停止休眠而拋出InterruptedException異常,從而提前使線程逃離阻塞狀態。

2.)對於正在運行的線程,即沒有阻塞的線程,調用Interrupt方法就只是把線程A的狀態改爲interruptted,但是不會影響線程A的繼續執行,除非線程A將interruptted狀態位作爲執行判斷符。

也就是說Interrupt只能有效地終止阻塞狀態下的線程,對於運行狀態下的線程需要退出標誌位來終止。下面結合代碼來說明如何終止兩種狀態下的線程

2.2 終止處於“阻塞狀態”的線程

通常,我們通過“中斷”方式終止處於“阻塞狀態”的線程。
當線程由於被調用了sleep(), wait(), join()等方法而進入阻塞狀態;若此時調用線程的interrupt()將線程的中斷標記設爲true。由於處於阻塞狀態,中斷標記會被清除(false),同時產生一個InterruptedException異常。將InterruptedException放在適當的爲止就能終止線程,形式如下:

public class myThread extends Thread{

    @Override
    public void run() {
        try {
            while (true) {
                System.out.println( "Thread is running..." );
                // 執行任務...
                Thread.sleep(1000);//lock.wait()
                .....
            }
        } catch (InterruptedException ie) {  
            // 由於產生InterruptedException異常,退出while(true)循環,線程終止!
            System.out.println("Thread exiting under request... ")
        }
 }
public class ThreadExample  {  
      
      public static void main( String args[] ) throws Exception {  
            MyThread thread = new EMyThread;  
            System.out.println( "Starting thread..." );  
            thread.start();  
            Thread.sleep( 3000 );  

            System.out.println( "Interrupting thread..." );  
            thread.interrupt();  

            Thread.sleep( 3000 );  
            System.out.println("Stopping application..." );  
            
      }  
}

控制檯打印

Starting thread... 

Thread running... 

Thread running... 

Thread running... 
 

Thread interrupted... 

Thread exiting under request... 

Stopping application... 

說明:在while(true)中不斷的執行任務,當線程處於阻塞狀態時,調用線程的interrupt()產生InterruptedException中斷。中斷的捕獲在while(true)之外,這樣就退出了while(true)循環!
注意:對InterruptedException的捕獲務一般放在while(true)循環體的外面,這樣,在產生異常時就退出了while(true)循環。否則,InterruptedException在while(true)循環體之內,就需要額外的添加退出處理,比如break。

2.3 終止處於“運行狀態”的線程

通常,我們通過“標記”方式終止處於“運行狀態”的線程。

其中,包括“中斷標記”和“額外添加標記”。

1. 通過“中斷標記”終止線程。

形式如下:

public class myThread extends Thread{

    @Override
    public void run() {
        while (!isInterrupted()) {
        // 執行任務...
        }
    }
}
public class ThreadShutdown {
    public static void main(String[] args) throws Exception {
       
        Thread myThread = new MyThread();
        myThread.start();
        
        //睡眠3秒,main線程對myThread進行中斷,使myThread能夠感知中斷而結束
        Thread.sleep(3000);
        myThread.interrupt();
      
}

說明:isInterrupted()是判斷線程的中斷標記是不是爲true。當線程處於運行狀態,並且我們需要終止它時;可以調用線程的interrupt()方法,使用線程的中斷標記爲true,即isInterrupted()會返回true。此時,就會退出while循環。
注意:interrupt()並不會終止處於“運行狀態”的線程!它會將線程的中斷標記設爲true。

2. 通過“額外添加標記”。

形式如下:

對背景中的代碼優化後:

class Example2 extends Thread {  
   
   volatile boolean stop = false;  

   public static void main( String args[] ) throws Exception {  
       Example2 thread = new Example2();  
       System.out.println( "Starting thread..." );  
       thread.start(); 
 
       Thread.sleep( 3000 );  
       System.out.println( "Asking thread to stop..." );  
  
       thread.stop = true;  
       Thread.sleep( 3000 );  
       System.out.println( "Stopping application..." );  
   
  }  
  
  public void run() {  
       while ( !stop ) {  
           System.out.println( "Thread is running..." );  
           long time = System.currentTimeMillis();  
           
           while ( (System.currentTimeMillis()-time < 1000) && (!stop) ) { 
               //執行任務...
           }  
       }  
       System.out.println( "Thread exiting under request..." );  
  }  

}  

控制檯打印:

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)中。

我們可以總結,調用線程類的interrupted方法,其本質只是設置該線程的中斷標誌,將中斷標誌設置爲true,並根據線程狀態決定是否拋出異常。因此,通過interrupted方法真正實現線程的中斷原理是:開發人員根據中斷標誌的具體值,來決定如何退出線程。


三、安全的終止線程

      通過上面的分析,正常運行中的線程可以通過退出標記安全的終止線程,當然,如果線程在運行過程中被阻塞,它便不能覈查共享變量,也就不能停止。這在許多情況下會 發生,例如調用Object.wait()、ServerSocket.accept()和DatagramSocket.receive()時。 這時就需要interrupt()拋出的InterruptedException來中斷線程。所以,綜合線程處於“阻塞狀態”和“運行狀態”的終止方式,是比較通用的安全終止線程的形式。

@Override
public void run() {
    try {
        // 1. isInterrupted()保證,只要中斷標記爲true就終止線程。
        while (!isInterrupted()) {
            // 執行任務...
        }
    } catch (InterruptedException ie) {  
        // 2. InterruptedException異常保證,當InterruptedException異常產生時,線程被終止。
    }
}

對背景代碼再一次修改

class Example3 extends Thread {  
   volatile boolean stop = false;
  
   public static void main( String args[] ) throws Exception {  
       Example3 thread = new Example3();  
       System.out.println( "Starting thread..." );  
       thread.start();  

       Thread.sleep( 3000 );  
       System.out.println( "Asking thread to stop..." );  
       thread.stop = true;//如果線程阻塞,將不會檢查此變量  
       thread.interrupt();  
       Thread.sleep( 3000 );  
       System.out.println( "Stopping application..." );  
   
   }  
  
   public void run() { 
       //只要兩個中斷標記之一爲true就終止線程。 
       while ( !stop && !Thread.currentThread().isInterrupted()) {  
           System.out.println( "Thread running..." );  
           try {  
               // 執行任務...
               // 由於某些原因可能被阻塞  
           } catch ( InterruptedException e ) {  
               //InterruptedException異常保證,當異常產生時,線程被終止。
               System.out.println( "Thread interrupted..." );  
           }  
       }  
       System.out.println( "Thread exiting under request..." );  
   }  
}  



 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章