本節(jié)主要討論一個(gè)問(wèn)題,如何在Java中取消或關(guān)閉一個(gè)線程?

取消/關(guān)閉的場(chǎng)景

我們知道,通過(guò)線程的start方法啟動(dòng)一個(gè)線程后,線程開(kāi)始執(zhí)行run方法,run方法運(yùn)行結(jié)束后線程退出,那為什么還需要結(jié)束一個(gè)線程呢?有多種情況,比如說(shuō):

  • 很多線程的運(yùn)行模式是死循環(huán),比如在生產(chǎn)者/消費(fèi)者模式中,消費(fèi)者主體就是一個(gè)死循環(huán),它不停的從隊(duì)列中接受任務(wù),執(zhí)行任務(wù),在停止程序時(shí),我們需要一種"優(yōu)雅"的方法以關(guān)閉該線程。

  • 在一些圖形用戶界面程序中,線程是用戶啟動(dòng)的,完成一些任務(wù),比如從遠(yuǎn)程服務(wù)器上下載一個(gè)文件,在下載過(guò)程中,用戶可能會(huì)希望取消該任務(wù)。

  • 在一些場(chǎng)景中,比如從第三方服務(wù)器查詢一個(gè)結(jié)果,我們希望在限定的時(shí)間內(nèi)得到結(jié)果,如果得不到,我們會(huì)希望取消該任務(wù)。

  • 有時(shí),我們會(huì)啟動(dòng)多個(gè)線程做同一件事,比如類似搶火車票,我們可能會(huì)讓多個(gè)好友幫忙從多個(gè)渠道買火車票,只要有一個(gè)渠道買到了,我們會(huì)通知取消其他渠道。

取消/關(guān)閉的機(jī)制

Java的Thread類定義了如下方法:

public final void stop()

這個(gè)方法看上去就可以停止線程,但這個(gè)方法被標(biāo)記為了過(guò)時(shí),簡(jiǎn)單的說(shuō),我們不應(yīng)該使用它,可以忽略它。

在Java中,停止一個(gè)線程的主要機(jī)制是中斷,中斷并不是強(qiáng)迫終止一個(gè)線程,它是一種協(xié)作機(jī)制,是給線程傳遞一個(gè)取消信號(hào),但是由線程來(lái)決定如何以及何時(shí)退出,本節(jié)我們主要就是來(lái)理解Java的中斷機(jī)制。

Thread類定義了如下關(guān)于中斷的方法:

public boolean isInterrupted()public void interrupt()public static boolean interrupted()

這三個(gè)方法名字類似,比較容易混淆,我們解釋一下。isInterrupted()和interrupt()是實(shí)例方法,調(diào)用它們需要通過(guò)線程對(duì)象,interrupted()是靜態(tài)方法,實(shí)際會(huì)調(diào)用Thread.currentThread()操作當(dāng)前線程。

每個(gè)線程都有一個(gè)標(biāo)志位,表示該線程是否被中斷了。

  • isInterrupted:就是返回對(duì)應(yīng)線程的中斷標(biāo)志位是否為true。

  • interrupted:返回當(dāng)前線程的中斷標(biāo)志位是否為true,但它還有一個(gè)重要的副作用,就是清空中斷標(biāo)志位,也就是說(shuō),連續(xù)兩次調(diào)用interrupted(),第一次返回的結(jié)果為true,第二次一般就是false (除非同時(shí)又發(fā)生了一次中斷)。

  • interrupt:表示中斷對(duì)應(yīng)的線程,中斷具體意味著什么呢?下面我們進(jìn)一步來(lái)說(shuō)明。

線程對(duì)中斷的反應(yīng)

interrupt()對(duì)線程的影響與線程的狀態(tài)和在進(jìn)行的IO操作有關(guān),我們先主要考慮線程的狀態(tài):

  • RUNNABLE:線程在運(yùn)行或具備運(yùn)行條件只是在等待操作系統(tǒng)調(diào)度

  • WAITING/TIMED_WAITING:線程在等待某個(gè)條件或超時(shí)

  • BLOCKED:線程在等待鎖,試圖進(jìn)入同步塊

  • NEW/TERMINATED:線程還未啟動(dòng)或已結(jié)束

RUNNABLE

如果線程在運(yùn)行中,且沒(méi)有執(zhí)行IO操作,interrupt()只是會(huì)設(shè)置線程的中斷標(biāo)志位,沒(méi)有任何其它作用。線程應(yīng)該在運(yùn)行過(guò)程中合適的位置檢查中斷標(biāo)志位,比如說(shuō),如果主體代碼是一個(gè)循環(huán),可以在循環(huán)開(kāi)始處進(jìn)行檢查,如下所示:

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

public class InterruptRunnableDemo extends Thread {
    @Override    public void run() {        while (!Thread.currentThread().isInterrupted()) {            // ... 單次循環(huán)代碼        }
        System.out.println("done ");
    }    public static void main(String[] args) throws InterruptedException {
        Thread thread = new InterruptRunnableDemo();
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

WAITING/TIMED_WAITING

線程執(zhí)行如下方法會(huì)進(jìn)入WAITING狀態(tài):

public final void join() throws InterruptedExceptionpublic final void wait() throws InterruptedException

執(zhí)行如下方法會(huì)進(jìn)入TIMED_WAITING狀態(tài):

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

在這些狀態(tài)時(shí),對(duì)線程對(duì)象調(diào)用interrupt()會(huì)使得該線程拋出InterruptedException,需要注意的是,拋出異常后,中斷標(biāo)志位會(huì)被清空,而不是被設(shè)置。比如說(shuō),執(zhí)行如下代碼:

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

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();

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

程序的輸出為false。

InterruptedException是一個(gè)受檢異常,線程必須進(jìn)行處理。我們?cè)?a target="_blank" style="text-decoration-line: none; color: rgb(51, 153, 255);">異常處理中介紹過(guò),處理異常的基本思路是,如果你知道怎么處理,就進(jìn)行處理,如果不知道,就應(yīng)該向上傳遞,通常情況下,你不應(yīng)該做的是,捕獲異常然后忽略。

捕獲到InterruptedException,通常表示希望結(jié)束該線程,線程大概有兩種處理方式:

  1. 向上傳遞該異常,這使得該方法也變成了一個(gè)可中斷的方法,需要調(diào)用者進(jìn)行處理。

  2. 有些情況,不能向上傳遞異常,比如Thread的run方法,它的聲明是固定的,不能拋出任何受檢異常,這時(shí),應(yīng)該捕獲異常,進(jìn)行合適的清理操作,清理后,一般應(yīng)該調(diào)用Thread的interrupt方法設(shè)置中斷標(biāo)志位,使得其他代碼有辦法知道它發(fā)生了中斷。

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

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

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

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

public class InterruptWaitingDemo extends Thread {
    @Override    public void run() {        while (!Thread.currentThread().isInterrupted()) {            try {                // 模擬任務(wù)代碼
                Thread.sleep(2000);
            } catch (InterruptedException e) {                // ... 清理操作                // 重設(shè)中斷標(biāo)志位                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();
    }
}

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

BLOCKED

如果線程在等待鎖,對(duì)線程對(duì)象調(diào)用interrupt()只是會(huì)設(shè)置線程的中斷標(biāo)志位,線程依然會(huì)處于BLOCKED狀態(tài),也就是說(shuō),interrupt()并不能使一個(gè)在等待鎖的線程真正"中斷"。我們看段代碼:

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

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();
    }
}

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

test方法在持有鎖lock的情況下啟動(dòng)線程a,而線程a也去嘗試獲得鎖lock,所以會(huì)進(jìn)入鎖等待隊(duì)列,隨后test調(diào)用線程a的interrupt方法并等待線程線程a結(jié)束,線程a會(huì)結(jié)束嗎?不會(huì),interrupt方法只會(huì)設(shè)置線程的中斷標(biāo)志,而并不會(huì)使它從鎖等待隊(duì)列中出來(lái)。

我們稍微修改下代碼,去掉test方法中的最后一行a.join,即變?yōu)椋?/p>

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

public static void test() throws InterruptedException {    synchronized (lock) {
        A a = new A();
        a.start();
        Thread.sleep(1000);

        a.interrupt();
    }
}

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

這時(shí),程序就會(huì)退出。為什么呢?因?yàn)橹骶€程不再等待線程a結(jié)束,釋放鎖lock后,線程a會(huì)獲得鎖,然后檢測(cè)到發(fā)生了中斷,所以會(huì)退出。

在使用synchronized關(guān)鍵字獲取鎖的過(guò)程中不響應(yīng)中斷請(qǐng)求,這是synchronized的局限性。如果這對(duì)程序是一個(gè)問(wèn)題,應(yīng)該使用顯式鎖,后面章節(jié)我們會(huì)介紹顯式鎖Lock接口,它支持以響應(yīng)中斷的方式獲取鎖。

NEW/TERMINATE

如果線程尚未啟動(dòng)(NEW),或者已經(jīng)結(jié)束(TERMINATED),則調(diào)用interrupt()對(duì)它沒(méi)有任何效果,中斷標(biāo)志位也不會(huì)被設(shè)置。比如說(shuō),以下代碼的輸出都是false。

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

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();
    }
}

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

IO操作

如果線程在等待IO操作,尤其是網(wǎng)絡(luò)IO,則會(huì)有一些特殊的處理,我們沒(méi)有介紹過(guò)網(wǎng)絡(luò),這里只是簡(jiǎn)單介紹下。

  • 如果IO通道是可中斷的,即實(shí)現(xiàn)了InterruptibleChannel接口,則線程的中斷標(biāo)志位會(huì)被設(shè)置,同時(shí),線程會(huì)收到異常ClosedByInterruptException。

  • 如果線程阻塞于Selector調(diào)用,則線程的中斷標(biāo)志位會(huì)被設(shè)置,同時(shí),阻塞的調(diào)用會(huì)立即返回。

我們重點(diǎn)介紹另一種情況,InputStream的read調(diào)用,該操作是不可中斷的,如果流中沒(méi)有數(shù)據(jù),read會(huì)阻塞 (但線程狀態(tài)依然是RUNNABLE),且不響應(yīng)interrupt(),與synchronized類似,調(diào)用interrupt()只會(huì)設(shè)置線程的中斷標(biāo)志,而不會(huì)真正"中斷"它,我們看段代碼。

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

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();
    }
}

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

線程t啟動(dòng)后調(diào)用System.in.read()從標(biāo)準(zhǔn)輸入讀入一個(gè)字符,不要輸入任何字符,我們會(huì)看到,調(diào)用interrupt()不會(huì)中斷read(),線程會(huì)一直運(yùn)行。

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

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

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();
    }
}

iOS培訓(xùn),Swift培訓(xùn),蘋果開(kāi)發(fā)培訓(xùn),移動(dòng)開(kāi)發(fā)培訓(xùn)

我們給線程定義了一個(gè)cancel方法,在該方法中,調(diào)用了流的close方法,同時(shí)調(diào)用了interrupt方法,這次,程序會(huì)輸出:

-1exit

也就是說(shuō),調(diào)用close方法后,read方法會(huì)返回,返回值為-1,表示流結(jié)束。

如何正確地取消/關(guān)閉線程

以上,我們可以看出,interrupt方法不一定會(huì)真正"中斷"線程,它只是一種協(xié)作機(jī)制,如果不明白線程在做什么,不應(yīng)該貿(mào)然的調(diào)用線程的interrupt方法,以為這樣就能取消線程。

對(duì)于以線程提供服務(wù)的程序模塊而言,它應(yīng)該封裝取消/關(guān)閉操作,提供單獨(dú)的取消/關(guān)閉方法給調(diào)用者,類似于InterruptReadDemo中演示的cancel方法,外部調(diào)用者應(yīng)該調(diào)用這些方法而不是直接調(diào)用interrupt。

Java并發(fā)庫(kù)的一些代碼就提供了單獨(dú)的取消/關(guān)閉方法,比如說(shuō),F(xiàn)uture接口提供了如下方法以取消任務(wù): 

boolean cancel(boolean mayInterruptIfRunning);

再比如,ExecutorService提供了如下兩個(gè)關(guān)閉方法:

void shutdown();
List<Runnable> shutdownNow();

Future和ExecutorService的API文檔對(duì)這些方法都進(jìn)行了詳細(xì)說(shuō)明,這是我們應(yīng)該學(xué)習(xí)的方式。關(guān)于這兩個(gè)接口,我們后續(xù)章節(jié)介紹。

小結(jié)

本節(jié)主要介紹了在Java中如何取消/關(guān)閉線程,主要依賴的技術(shù)是中斷,但它是一種協(xié)作機(jī)制,不會(huì)強(qiáng)迫終止線程,我們介紹了線程在不同狀態(tài)和IO操作時(shí)對(duì)中斷的反應(yīng),作為線程的實(shí)現(xiàn)者,應(yīng)該提供明確的取消/關(guān)閉方法,并用文檔描述清楚其行為,作為線程的調(diào)用者,應(yīng)該使用其取消/關(guān)閉方法,而不是貿(mào)然調(diào)用interrupt。

65節(jié)到本節(jié),我們介紹的都是關(guān)于線程的基本內(nèi)容,在Java中還有一套并發(fā)工具包,位于包java.util.concurrent下,里面包括很多易用且高性能的并發(fā)開(kāi)發(fā)工具,從下一節(jié)開(kāi)始,我們就來(lái)討論它,先從最基本的原子變量和CAS操作開(kāi)始。

(與其他章節(jié)一樣,本節(jié)所有代碼位于 https://github.com/swiftma/program-logic)

----------------

未完待續(xù),查看最新文章,敬請(qǐng)關(guān)注微信公眾號(hào)“老馬說(shuō)編程”(掃描下方二維碼),從入門到高級(jí),深入淺出,老馬和你一起探索Java編程及計(jì)算機(jī)技術(shù)的本質(zhì)。用心原創(chuàng),保留所有版權(quán)。

http://www.cnblogs.com/swiftma/p/6477189.html