Thread之十:中止線程方法彙總

在上篇文章《多線程的使用——Thread類和Runnable接口》中提到中斷線程的問題。在JAVA中,曾經使用stop方法來中止線程,然而,該方法具備固有的不安全性,於是已經被拋棄(Deprecated)。那麼應該怎麼結束一個進程呢?官方文檔中對此有詳細說明:《爲什麼不同意使用 Thread.stop、Thread.suspend 和 Thread.resume?》。在此引用stop方法的說明:html

1. Why is Thread.stop deprecated?
Because it is inherently unsafe. Stopping a thread causes it to unlock all the monitors that it has locked. (The monitors are unlocked as the ThreadDeath exception propagates up the stack.) If any of the objects previously protected by these monitors were in an inconsistent state, other threads may now view these objects in an inconsistent state. Such objects are said to be damaged. When threads operate on damaged objects, arbitrary behavior can result. This behavior may be subtle and difficult to detect, or it may be pronounced. Unlike other unchecked exceptions, ThreadDeath kills threads silently; thus, the user has no warning that his program may be corrupted. The corruption can manifest itself at any time after the actual damage occurs, even hours or days in the future.
大概意思是:
由於該方法本質上是不安全的。中止一個線程將釋放它已經鎖定的全部監視器(做爲沿堆棧向上傳播的未檢查 ThreadDeath 異常的一個天然後果)。若是之前受這些監視器保護的任何對象都處於一種不一致的狀態,則損壞的對象將對其餘線程可見,這有可能致使任意的行爲。此行爲多是微妙的,難以察覺,也多是顯著的。不像其餘的未檢查異常,ThreadDeath異常會在後臺殺死線程,所以,用戶並不會獲得警告,提示他的程序可能已損壞。這種損壞有可能在實際破壞發生以後的任什麼時候間表現出來,也有可能在多小時甚至在將來的不少天后。
在文檔中還提到,程序員不能經過捕獲ThreadDeath異常來修復已破壞的對象。具體緣由見原文。
既然stop方法不建議使用,那麼應該用什麼方法來代理stop已實現相應的功能呢?
 
一、經過修改共享變量來通知目標線程中止運行
 
大部分須要使用stop的地方應該使用這種方法來達到中斷線程的目的。
這種方法有幾個要求或注意事項:
(1)目標線程必須有規律的檢查變量,當該變量指示它應該中止運行時,該線程應該按必定的順序從它執行的方法中返回。
(2)該變量必須定義爲 volatile或者全部對它的訪問必須同步(synchronized)。
例如:
package chapter2; 

public class ThreadFlag extends Thread 
{ 
    public volatile boolean exit = false; 

    public void run() 
    { 
        while (!exit); 
    } 
    public static void main(String[] args) throws Exception 
    { 
        ThreadFlag thread = new ThreadFlag(); 
        thread.start(); 
        sleep(5000); // 主線程延遲5秒 
        thread.exit = true;  // 終止線程thread 
        thread.join(); 
        System.out.println("線程退出!"); 
    } 
} 

 在上面代碼中定義了一個退出標誌exit,當exit爲true時,while循環退出,exit的默認值爲false.在定義exit時,使用了一個Java關鍵字volatile,這個關鍵字的目的是使exit同步,也就是說在同一時刻只能由一個線程來修改exit的值。java

二、經過Thread.interrupt方法中斷線程
一般狀況下,咱們應該使用第一種方式來代替Thread.stop方法。然而如下幾種方式應該使用Thread.interrupt方法來中斷線程(該方法一般也會結合第一種方法使用)。
一開始使用interrupt方法時,會有莫名奇妙的感受:難道該方法有問題?
API文檔上說,該方法用於"Interrupts this thread"。請看下面的例子:
package com.jvm.study.thread;

public class TestThread implements Runnable{ 
     
    boolean stop = false; 
    public static void main(String[] args) throws Exception { 
        Thread thread = new Thread(new TestThread(),"My Thread"); 
        System.out.println( "Starting thread..." ); 
        thread.start(); 
        Thread.sleep( 3000 ); 
        System.out.println( "Interrupting thread..." ); 
        thread.interrupt(); 
        System.out.println("線程是否中斷:" + thread.isInterrupted()); 
        Thread.sleep( 3000 ); 
        System.out.println("Stopping application..." ); 
    } 
    public void run() { 
        while(!stop){ 
            System.out.println( "My Thread is running..." ); 
            // 讓該循環持續一段時間,使上面的話打印次數少點 
            long time = System.currentTimeMillis(); 
            while((System.currentTimeMillis()-time < 1000)) { 
            } 
        } 
        System.out.println("My Thread exiting under request..." ); 
    } 
} 
運行後的結果是:
Starting thread...
My Thread is running...
My Thread is running...
My Thread is running...
My Thread is running...
Interrupting thread...
線程是否中斷:true
My Thread is running...
My Thread is running...
My Thread is running...
Stopping application...
My Thread is running...
My Thread is running...
……
應用程序並不會退出,啓動的線程沒有由於調用interrupt而終止,但是從調用isInterrupted方法返回的結果能夠清楚地知道該線程已經中斷了。那位什麼會出現這種狀況呢?究竟是interrupt方法出問題了仍是isInterrupted方法出問題了?在Thread類中還有一個測試中斷狀態的方法(靜態的)interrupted,換用這個方法測試,獲得的結果是同樣的。由此彷佛應該是interrupt方法出問題了。實際上,在JAVA API文檔中對該方法進行了詳細的說明。該方法實際上只是設置了一箇中斷狀態, 當該線程因爲下列緣由而受阻時,這個中斷狀態就能起做用了
(1)若是線程在調用 Object 類的 wait()、wait(long) 或 wait(long, int) 方法,或者該類的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法過程當中受阻,則其中斷狀態將被清除,它還將收到一個InterruptedException異常。這個時候,咱們能夠經過捕獲InterruptedException異常來終止線程的執行,具體能夠經過return等退出或改變共享變量的值使其退出。
(2)若是該線程在可中斷的通道上的 I/O 操做中受阻,則該通道將被關閉,該線程的中斷狀態將被設置而且該線程將收到一個 ClosedByInterruptException。這時候處理方法同樣,只是捕獲的異常不同而已。
其實對於這些狀況有一個通用的處理方法:
package com.jvm.study.thread;

public class TestThread2 implements Runnable {

    boolean stop = false;

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new TestThread2(), "My Thread2");
        System.out.println("Starting thread...");
        thread.start();
        Thread.sleep(10000);
        System.out.println("Interrupting thread...");
        thread.interrupt();
        System.out.println("線程是否中斷:" + thread.isInterrupted());
        Thread.sleep(3000);
        System.out.println("Stopping application...");
        Thread.sleep(30000);
    }

    public void run() {
        while (!stop) {
            System.out.println("My Thread is running...");
            // 讓該循環持續一段時間,使上面的話打印次數少點
            long time = System.currentTimeMillis();
            while ((System.currentTimeMillis() - time < 1000)) {
            }
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
        }
        System.out.println("My Thread exiting under request...");
    }
}
由於調用interrupt方法後,會設置線程的中斷狀態,因此,經過監視該狀態來達到終止線程的目的。這些在《 Thread之八:interrupt中斷》也提到過。
 
總結:程序應該對線程中斷做出恰當的響應。響應方式一般有三種:(來自溫紹錦(暱稱:溫少):http//www.cnblogs.com/jobs/)

注意:interrupted與isInterrupted方法的區別(見API文檔)程序員

 

 當線程處於下面的Blocked狀態,經過調用interrupt()方法來中止線程:編程

2.一、當線程處於下面的情況時(1)若是線程在調用 Object 類的 wait()、wait(long) 或 wait(long, int) 方法,或者該類的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法過程當中受阻,則其中斷狀態將被清除,它還將收到一個InterruptedException異常。這個時候,咱們能夠經過捕獲InterruptedException異常來終止線程的執行,具體能夠經過return等退出或改變共享變量的值使其退出。),屬於非運行狀態:api

  • 當sleep方法被調用。緩存

  • 當wait方法被調用。安全

  • 當被I/O阻塞,多是文件或者網絡等等。網絡

當線程處於上述的狀態時,使用前面介紹的方法就不可用了。這個時候,咱們可使用interrupt()來打破阻塞的狀況,如:多線程

public void stop() {
        Thread tmpBlinker = blinker;
        blinker = null;
        if (tmpBlinker != null) {
           tmpBlinker.interrupt();
        }
    }

 

interrupt()被調用的時候,InterruptedException將被拋出,因此你能夠再run方法中捕獲這個異常,讓線程安全退出:oracle

try {
   ....
   wait();
} catch (InterruptedException iex) {
   throw new RuntimeException("Interrupted",iex);
}

 

2.二、阻塞的I/O((2)若是該線程在可中斷的通道上的 I/O 操做中受阻,則該通道將被關閉,該線程的中斷狀態將被設置而且該線程將收到一個 ClosedByInterruptException。這時候處理方法同樣,只是捕獲的異常不同而已

當線程被I/O阻塞的時候,調用interrupt()的狀況是依賴與實際運行的平臺的。在Solaris和Linux平臺上將會拋出InterruptedIOException的異常,可是Windows上面不會有這種異常。因此,咱們處理這種問題不能依靠於平臺的實現。如:

package com.cnblogs.gpcuster

import java.net.*;
import java.io.*;

public abstract class InterruptibleReader extends Thread {

    private Object lock = new Object( );
    private InputStream is;
    private boolean done;
    private int buflen;
    protected void processData(byte[] b, int n) { }

    class ReaderClass extends Thread {

        public void run( ) {
            byte[] b = new byte[buflen];

            while (!done) {
                try {
                    int n = is.read(b, 0, buflen);
                    processData(b, n);
                } catch (IOException ioe) {
                    done = true;
                }
            }

            synchronized(lock) {
                lock.notify( );
            }
        }
    }

    public InterruptibleReader(InputStream is) {
        this(is, 512);
    }

    public InterruptibleReader(InputStream is, int len) {
        this.is = is;
        buflen = len;
    }

    public void run( ) {
        ReaderClass rc = new ReaderClass( );

        synchronized(lock) {
            rc.start( );
            while (!done) {
                try {
                    lock.wait( );
                } catch (InterruptedException ie) {
                    done = true;
                    rc.interrupt( );
                    try {
                        is.close( );
                    } catch (IOException ioe) {}
                }
            }
        }
    }
} 

另外,咱們也可使用InterruptibleChannel接口。 實現了InterruptibleChannel接口的類能夠在阻塞的時候拋出ClosedByInterruptException。如:

package com.cnblogs.gpcuster

import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.channels.Channels;

public class InterruptInput {   
    static BufferedReader in = new BufferedReader(
            new InputStreamReader(
            Channels.newInputStream(
            (new FileInputStream(FileDescriptor.in)).getChannel())));
    
    public static void main(String args[]) {
        try {
            System.out.println("Enter lines of input (user ctrl+Z Enter to terminate):");
            System.out.println("(Input thread will be interrupted in 10 sec.)");
            // interrupt input in 10 sec
            (new TimeOut()).start();
            String line = null;
            while ((line = in.readLine()) != null) {
                System.out.println("Read line:'"+line+"'");
            }
        } catch (Exception ex) {
            System.out.println(ex.toString()); // printStackTrace();
        }
    }
    
    public static class TimeOut extends Thread {
        int sleepTime = 10000;
        Thread threadToInterrupt = null;    
        public TimeOut() {
            // interrupt thread that creates this TimeOut.
            threadToInterrupt = Thread.currentThread();
            setDaemon(true);
        }
        
        public void run() {
            try {
                sleep(10000); // wait 10 sec
            } catch(InterruptedException ex) {/*ignore*/}
            threadToInterrupt.interrupt();
        }
    }
}

 

這裏還須要注意一點,當線程處於寫文件的狀態時,調用interrupt()不會中斷線程。

 

3、不提倡的stop()方法 
臭名昭著的stop()中止線程的方法已不提倡使用了,緣由是什麼呢?
 當在一個線程對象上調用stop()方法時,這個線程對象所運行的線程就會當即中止,並拋出特殊的ThreadDeath()異常。這裏的「當即」由於太「當即」了,

    在java多線程編程中,線程的終止能夠說是一個必然會遇到的操做。可是這樣一個常見的操做其實並非一個可以垂手可得實現的操做,並且在某些場景下狀況會變得更復雜更棘手。

        Java標準API中的Thread類提供了stop方法能夠終止線程,可是很遺憾,這種方法不建議使用,緣由是這種方式終止線程中斷臨界區代碼執行,並會釋放線程以前獲取的監控器鎖,這樣勢必引發某些對象狀態的不一致(由於臨界區代碼通常是原子的,不會被幹擾的),具體緣由能夠參考資料[1]。這樣一來,就必須根據線程的特色使用不一樣的替代方案以終止線程。根據中止線程時線程執行狀態的不一樣有以下中止線程的方法。

Thread之九:stop

4、 處於運行狀態的線程中止

        處於運行狀態的線程就是常見的處於一個循環中不斷執行業務流程的線程,這樣的線程須要經過設置中止變量的方式,在每次循環開始處判斷變量是否改變爲中止,以達到中止線程的目的,好比以下代碼框架:

private volatile Thread blinker;  
public void stop() {  
        blinker = null;  
}  
public void run() {  
        Thread thisThread = Thread.currentThread();  
        while (blinker == thisThread) {  
            try {  
                //業務流程  
            } catch (Exception e){}  
        }  
}

 

        若是主線程調用該線程對象的stop方法,blinker對象被設置爲null,則線程的下次循環中blinker!=thisThread,於是能夠退出循環,並退出run方法而使線程結束。將引用變量blinker的類型前加上volatile關鍵字的目的是防止編譯器對該變量存取時的優化,這種優化主要是緩存對變量的修改,這將使其餘線程不會馬上看到修改後的blinker值,從而影響退出。此外,Java標準保證被volatile修飾的變量的讀寫都是原子的。

        上述的Thread類型的blinker徹底能夠由更爲簡單的boolean類型變量代替。

5、 即將或正在處於非運行態的線程中止

        線程的非運行狀態常見的有以下兩種狀況:

可中斷等待:線程調用了sleep或wait方法,這些方法可拋出InterruptedException;

Io阻塞:線程調用了IO的read操做或者socket的accept操做,處於阻塞狀態。

上面已經講過。

3 處於大數據IO讀寫中的線程中止

         處於大數據IO讀寫中的線程實際上處於運行狀態,而不是等待或阻塞狀態,所以上面的interrupt機制不適用。線程處於IO讀寫中能夠當作是線程運行中的一種特例。中止這樣的線程的辦法是強行close掉io輸入輸出流對象,使其拋出異常,進而使線程中止。

        最好的建議是將大數據的IO讀寫操做放在循環中進行,這樣能夠在每次循環中都有線程中止的時機,這也就將問題轉化爲如何中止正在運行中的線程的問題了。

4 在線程運行前中止線程

         有時,線程中的run方法須要足夠健壯以支持在線程實際運行前終止線程的狀況。即在Thread建立後,到Thread的start方法調用前這段時間,調用自定義的stop方法也要奏效。從上述的中止處於等待狀態線程的代碼示例中,stop方法並不能終止運行前的線程,由於在Thread的start方法被調用前,調用interrupt方法並不會將Thread對象的中斷狀態置位,這樣當run方法執行時,currentThread的isInterrupted方法返回false,線程將繼續執行下去。

         爲了解決這個問題,不得不本身再額外建立一個volatile標誌量,並將其加入run方法的最開頭:

public void run() {  
        if (myThread == null) {  
           return; // stopped before started.  
        }  
        while(!Thread.currentThread().isInterrupted()){  
    //業務邏輯  
        }  
}  

 

         還有一種解決方法,也能夠在run中直接使用該自定義標誌量,而不使用isInterrupted方法判斷線程是否應該中止。這種方法的run代碼框架實際上和中止運行時線程的同樣。

相關文章
相關標籤/搜索