Java 線程的終止-interrupt

來源:互聯網
上載者:User

標籤:消費者   body   strong   樣本   情境   關鍵字   res   停止線程   有一個   

Java線程的終止——interrupt

取消/關閉的情境

我們知道,通過線程的start方法啟動一個線程後,線程開始執行run方法,run方法運行結束後線程退出,那為什麼還需要結束一個線程呢?有多種情況,比如說: 
很多線程的運行模式是死迴圈,比如在生產者/消費者模式中,消費者主體就是一個死迴圈,它不停的從隊列中接受任務,執行任務,在停止程式時,我們需要一種”優雅”的方法以關閉該線程。 
在一些圖形化使用者介面程式中,線程是使用者啟動的,完成一些任務,比如從遠程伺服器上下載一個檔案,在下載過程中,使用者可能會希望取消該任務。 
在一些情境中,比如從第三方伺服器查詢一個結果,我們希望在限定的時間內得到結果,如果得不到,我們會希望取消該任務。 
有時,我們會啟動多個線程做同一件事,比如類似搶火車票,我們可能會讓多個好友幫忙從多個渠道買火車票,只要有一個渠道買到了,我們會通知取消其他渠道。

取消/關閉的機制 
Java的Thread類定義了如下方法: 
public final void stop()

這個方法看上去就可以停止線程,但這個方法被標記為了過時,簡單的說,我們不應該使用它,可以忽略它。

在Java中,停止一個線程的主要機制是中斷,中斷並不是強迫終止一個線程,它是一種協作機制,是給線程傳遞一個取消訊號,但是由線程來決定如何以及何時退出,本節我們主要就是來理解Java的中斷機制。

Thread類定義了如下關於中斷的方法: 
public boolean isInterrupted() 
public void interrupt() 
public static boolean interrupted()

這三個方法名字類似,比較容易混淆,我們解釋一下。isInterrupted()和interrupt()是執行個體方法,調用它們需要通過線程對象,interrupted()是靜態方法,實際會調用Thread.currentThread()操作當前線程。

每個線程都有一個標誌位,表示該線程是否被中斷了。 
isInterrupted:就是返回對應線程的中斷標誌位是否為true。 
interrupted:返回當前線程的中斷標誌位是否為true,但它還有一個重要的副作用,就是清空中斷標誌位,也就是說,連續兩次調用interrupted(),第一次返回的結果為true,第二次一般就是false (除非同時又發生了一次中斷)。 
interrupt:表示中斷對應的線程,中斷具體意味著什麼呢?下面我們進一步來說明。

線程對中斷的反應 
interrupt()對線程的影響與線程的狀態和在進行的IO操作有關,我們先主要考慮線程的狀態: 
RUNNABLE:線程在運行或具備運行條件只是在等待作業系統調度 
WAITING/TIMED_WAITING:線程在等待某個條件或逾時 
BLOCKED:線程在等待鎖,試圖進入同步塊 
NEW/TERMINATED:線程還未啟動或已結束

RUNNABLE 
如果線程在運行中,且沒有執行IO操作,interrupt()只是會設定線程的中斷標誌位,沒有任何其它作用。線程應該在運行過程中合適的位置檢查中斷標誌位,比如說,如果主體代碼是一個迴圈,可以在迴圈開始處進行檢查,如下所示:

public class InterruptRunnableDemo extends Thread {    @Override    public void run() {        while (!Thread.currentThread().isInterrupted()) {            // ... 單次迴圈代碼        }        System.out.println("done ");    }    public static void main(String[] args) throws InterruptedException {        Thread thread = new InterruptRunnableDemo();        thread.start();        Thread.sleep(1000);        thread.interrupt();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

WAITING/TIMED_WAITING 
線程執行如下方法會進入WAITING狀態: 
public final void join() throws InterruptedException 
public final void wait() throws InterruptedException

執行如下方法會進入TIMED_WAITING狀態: 
public final native void wait(long timeout) throws InterruptedException; 
public static native void sleep(long millis) throws InterruptedException; 
public final synchronized void join(long millis) throws InterruptedException

在這些狀態時,對線程對象調用interrupt()會使得該線程拋出InterruptedException,需要注意的是,拋出異常後,中斷標誌位會被清空,而不是被設定。比如說,執行如下代碼:

Thread t = new Thread (){    @Override    public void run() {        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            System.out.println(isInterrupted());        }    }        };t.start();try {    Thread.sleep(100);} catch (InterruptedException e) {}t.interrupt();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

程式的輸出為false。

InterruptedException是一個受檢異常,線程必須進行處理。我們在異常處理中介紹過,處理異常的基本思路是,如果你知道怎麼處理,就進行處理,如果不知道,就應該向上傳遞,通常情況下,你不應該做的是,捕獲異常然後忽略。

捕獲到InterruptedException,通常表示希望結束該線程,線程大概有兩種處理方式: 
向上傳遞該異常,這使得該方法也變成了一個可中斷的方法,需要調用者進行處理。 
有些情況,不能向上傳遞異常,比如Thread的run方法,它的聲明是固定的,不能拋出任何受檢異常,這時,應該捕獲異常,進行合適的清理操作,清理後,一般應該調用Thread的interrupt方法設定中斷標誌位,使得其他代碼有辦法知道它發生了中斷。

第一種方式的範例程式碼如下:

public void interruptibleMethod() throws InterruptedException{    // ... 包含wait, join 或 sleep 方法    Thread.sleep(1000);}
  • 1
  • 2
  • 3
  • 4

第二種方式的範例程式碼如下:

public class InterruptWaitingDemo extends Thread {    @Override    public void run() {        while (!Thread.currentThread().isInterrupted()) {            try {                // 類比任務代碼                Thread.sleep(2000);            } catch (InterruptedException e) {                // ... 清理操作                // 重設中斷標誌位                Thread.currentThread().interrupt();            }        }        System.out.println(isInterrupted());    }    public static void main(String[] args) {        InterruptWaitingDemo thread = new InterruptWaitingDemo();        thread.start();        try {            Thread.sleep(100);        } catch (InterruptedException e) {        }        thread.interrupt();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

BLOCKED 
如果線程在等待鎖,對線程對象調用interrupt()只是會設定線程的中斷標誌位,線程依然會處於BLOCKED狀態,也就是說,interrupt()並不能使一個在等待鎖的線程真正”中斷”。我們看段代碼:

public class InterruptSynchronizedDemo {    private static Object lock = new Object();    private static class A extends Thread {        @Override        public void run() {            synchronized (lock) {                while (!Thread.currentThread().isInterrupted()) {                }            }            System.out.println("exit");        }    }    public static void test() throws InterruptedException {        synchronized (lock) {            A a = new A();            a.start();            Thread.sleep(1000);            a.interrupt();            a.join();        }    }    public static void main(String[] args) throws InterruptedException {        test();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

test方法在持有鎖lock的情況下啟動線程a,而線程a也去嘗試獲得鎖lock,所以會進入鎖等待隊列,隨後test調用線程a的interrupt方法並等待線程線程a結束,線程a會結束嗎?不會,interrupt方法只會設定線程的中斷標誌,而並不會使它從鎖等待隊列中出來。

我們稍微修改下代碼,去掉test方法中的最後一行a.join,即變為:

public static void test() throws InterruptedException {    synchronized (lock) {        A a = new A();        a.start();        Thread.sleep(1000);        a.interrupt();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

這時,程式就會退出。為什麼呢?因為主線程不再等待線程a結束,釋放鎖lock後,線程a會獲得鎖,然後檢測到發生了中斷,所以會退出。

在使用synchronized關鍵字擷取鎖的過程中不響應插斷要求,這是synchronized的局限性。如果這對程式是一個問題,應該使用顯式鎖,後面章節我們會介紹顯式鎖Lock介面,它支援以響應中斷的方式擷取鎖。

NEW/TERMINATE 
如果線程尚未啟動(NEW),或者已經結束(TERMINATED),則調用interrupt()對它沒有任何效果,中斷標誌位也不會被設定。比如說,以下代碼的輸出都是false。

public class InterruptNotAliveDemo {    private static class A extends Thread {        @Override        public void run() {        }    }    public static void test() throws InterruptedException {        A a = new A();        a.interrupt();        System.out.println(a.isInterrupted());        a.start();        Thread.sleep(100);        a.interrupt();        System.out.println(a.isInterrupted());    }    public static void main(String[] args) throws InterruptedException {        test();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

IO操作 
如果線程在等待IO操作,尤其是網路IO,則會有一些特殊的處理,我們沒有介紹過網路,這裡只是簡單介紹下。 
如果IO通道是可中斷的,即實現了InterruptibleChannel介面,則線程的中斷標誌位會被設定,同時,線程會收到異常ClosedByInterruptException。 
如果線程阻塞於Selector調用,則線程的中斷標誌位會被設定,同時,阻塞的調用會立即返回。

我們重點介紹另一種情況,InputStream的read調用,該操作是不可中斷的,如果流中沒有資料,read會阻塞 (但線程狀態依然是RUNNABLE),且不響應interrupt(),與synchronized類似,調用interrupt()只會設定線程的中斷標誌,而不會真正”中斷”它,我們看段代碼。

public class InterruptReadDemo {    private static class A extends Thread {        @Override        public void run() {            while(!Thread.currentThread().isInterrupted()){                try {                    System.out.println(System.in.read());                } catch (IOException e) {                    e.printStackTrace();                }                }            System.out.println("exit");        }    }    public static void main(String[] args) throws InterruptedException {        A t = new A();        t.start();        Thread.sleep(100);        t.interrupt();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

線程t啟動後調用System.in.read()從標準輸入讀入一個字元,不要輸入任何字元,我們會看到,調用interrupt()不會中斷read(),線程會一直運行。

不過,有一個辦法可以中斷read()調用,那就是調用流的close方法,我們將代碼改為:

public class InterruptReadDemo {    private static class A extends Thread {        @Override        public void run() {            while (!Thread.currentThread().isInterrupted()) {                try {                    System.out.println(System.in.read());                } catch (IOException e) {                    e.printStackTrace();                }            }            System.out.println("exit");        }        public void cancel() {            try {                System.in.close();            } catch (IOException e) {            }            interrupt();        }    }    public static void main(String[] args) throws InterruptedException {        A t = new A();        t.start();        Thread.sleep(100);        t.cancel();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

我們給線程定義了一個cancel方法,在該方法中,調用了流的close方法,同時調用了interrupt方法,這次,程式會輸出:

-1exit
  • 1
  • 2

也就是說,調用close方法後,read方法會返回,傳回值為-1,表示流結束。

如何正確地取消/關閉線程

有三種方法可以使終止線程。

1.  使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止。 2.  使用stop方法強行終止線程(這個方法不推薦使用,因為stop和suspend、resume一樣,也可能發生不可預料的結果)。 3.  使用interrupt方法中斷線程。

以上,我們可以看出,interrupt方法不一定會真正”中斷”線程,它只是一種協作機制,如果不明白線程在做什麼,不應該貿然的調用線程的interrupt方法,以為這樣就能取消線程。

對於以線程提供服務的程式模組而言,它應該封裝取消/關閉操作,提供單獨的取消/關閉方法給調用者,類似於InterruptReadDemo中示範的cancel方法,外部調用者應該調用這些方法而不是直接調用interrupt。

Java並發庫的一些代碼就提供了單獨的取消/關閉方法,比如說,Future介面提供了如下方法以取消任務: 
boolean cancel(boolean mayInterruptIfRunning);

再比如,ExecutorService提供了如下兩個關閉方法: 
void shutdown(); 
List shutdownNow();

Future和ExecutorService的API文檔對這些方法都進行了詳細說明,這是我們應該學習的方式。關於這兩個介面,我們後續章節介紹。

小結 
本節主要介紹了在Java中如何取消/關閉線程,主要依賴的技術是中斷,但它是一種協作機制,不會強迫終止線程,我們介紹了線程在不同狀態和IO操作時對中斷的反應,作為線程的實現者,應該提供明確的取消/關閉方法,並用文檔描述清楚其行為,作為線程的調用者,應該使用其取消/關閉方法,而不是貿然調用interrupt

 

Java 線程的終止-interrupt

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.