本節(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)行檢查,如下所示:
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(); } }
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í)行如下代碼:
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是一個(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é)束該線程,線程大概有兩種處理方式:
向上傳遞該異常,這使得該方法也變成了一個(gè)可中斷的方法,需要調(diào)用者進(jìn)行處理。
有些情況,不能向上傳遞異常,比如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); }
第二種方式的示例代碼如下:
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(); } }
BLOCKED
如果線程在等待鎖,對(duì)線程對(duì)象調(diào)用interrupt()只是會(huì)設(shè)置線程的中斷標(biāo)志位,線程依然會(huì)處于BLOCKED狀態(tài),也就是說(shuō),interrupt()并不能使一個(gè)在等待鎖的線程真正"中斷"。我們看段代碼:
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的情況下啟動(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>
public static void test() throws InterruptedException { synchronized (lock) { A a = new A(); a.start(); Thread.sleep(1000); a.interrupt(); } }
這時(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。
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操作,尤其是網(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ì)真正"中斷"它,我們看段代碼。
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啟動(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方法,我們將代碼改為:
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(); } }
我們給線程定義了一個(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