計算機程序的思惟邏輯 (69) - 線程的中斷

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html

本節主要討論一個問題,如何在Java中取消或關閉一個線程?java

取消/關閉的場景

咱們知道,經過線程的start方法啓動一個線程後,線程開始執行run方法,run方法運行結束後線程退出,那爲何還須要結束一個線程呢?有多種狀況,好比說:git

  • 不少線程的運行模式是死循環,好比在生產者/消費者模式中,消費者主體就是一個死循環,它不停的從隊列中接受任務,執行任務,在中止程序時,咱們須要一種"優雅"的方法以關閉該線程。
  • 在一些圖形用戶界面程序中,線程是用戶啓動的,完成一些任務,好比從遠程服務器上下載一個文件,在下載過程當中,用戶可能會但願取消該任務。
  • 在一些場景中,好比從第三方服務器查詢一個結果,咱們但願在限定的時間內獲得結果,若是得不到,咱們會但願取消該任務。
  • 有時,咱們會啓動多個線程作同一件事,好比相似搶火車票,咱們可能會讓多個好友幫忙從多個渠道買火車票,只要有一個渠道買到了,咱們會通知取消其餘渠道。

取消/關閉的機制

Java的Thread類定義了以下方法:github

public final void stop() 複製代碼

這個方法看上去就能夠中止線程,但這個方法被標記爲了過期,簡單的說,咱們不該該使用它,能夠忽略它。編程

在Java中,中止一個線程的主要機制是中斷,中斷並非強迫終止一個線程,它是一種協做機制,是給線程傳遞一個取消信號,可是由線程來決定如何以及什麼時候退出,本節咱們主要就是來理解Java的中斷機制。swift

Thread類定義了以下關於中斷的方法:bash

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();
    }
}
複製代碼

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();
複製代碼

程序的輸出爲false。

InterruptedException是一個受檢異常,線程必須進行處理。咱們在異常處理中介紹過,處理異常的基本思路是,若是你知道怎麼處理,就進行處理,若是不知道,就應該向上傳遞,一般狀況下,你不該該作的是,捕獲異常而後忽略。

捕獲到InterruptedException,一般表示但願結束該線程,線程大概有兩種處理方式:

  1. 向上傳遞該異常,這使得該方法也變成了一個可中斷的方法,須要調用者進行處理。
  2. 有些狀況,不能向上傳遞異常,好比Thread的run方法,它的聲明是固定的,不能拋出任何受檢異常,這時,應該捕獲異常,進行合適的清理操做,清理後,通常應該調用Thread的interrupt方法設置中斷標誌位,使得其餘代碼有辦法知道它發生了中斷。

第一種方式的示例代碼以下:

public void interruptibleMethod() throws InterruptedException{
    // ... 包含wait, join 或 sleep 方法
    Thread.sleep(1000);
}
複製代碼

第二種方式的示例代碼以下:

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();
    }
}
複製代碼

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();
    }
}
複製代碼

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();
    }
}
複製代碼

這時,程序就會退出。爲何呢?由於主線程再也不等待線程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();
    }
}
複製代碼

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();
    }
}
複製代碼

線程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();
    }
}
複製代碼

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

-1
exit
複製代碼

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

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

以上,咱們能夠看出,interrupt方法不必定會真正"中斷"線程,它只是一種協做機制,若是不明白線程在作什麼,不該該貿然的調用線程的interrupt方法,覺得這樣就能取消線程。

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

Java併發庫的一些代碼就提供了單獨的取消/關閉方法,好比說,Future接口提供了以下方法以取消任務:

boolean cancel(boolean mayInterruptIfRunning);
複製代碼

再好比,ExecutorService提供了以下兩個關閉方法:

void shutdown();
List<Runnable> shutdownNow();
複製代碼

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

小結

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

65節到本節,咱們介紹的都是關於線程的基本內容,在Java中還有一套併發工具包,位於包java.util.concurrent下,裏面包括不少易用且高性能的併發開發工具,從下一節開始,咱們就來討論它,先從最基本的原子變量和CAS操做開始。

(與其餘章節同樣,本節全部代碼位於 github.com/swiftma/pro…)


未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索