在本博文的一篇如何正確的關閉一個線程一文中講解了如何利用interrupt機制來中斷一個線程,這篇文章當時確實花了一些精力的總結,不過都是15年末的事情了,現在是2017年2月份,經過一年的時間,決定重新寫一篇完善的關于線程中斷的文章。
下面簡單的舉例情況:
比如我們會啟動多個線程做同一件事,比如搶12306的火車票,我們可能開啟多個線程從多個渠道買火車票,只要有一個渠道買到了,我們會通知取消其他渠道。這個時候需要關閉其他線程很多線程的運行模式是死循環,比如在生產者/消費者模式中,消費者主體就是一個死循環,它不停的從隊列中接受任務,執行任務,在停止程序時,我們需要一種”優雅”的方法以關閉該線程在一些場景中,比如從第三方服務器查詢一個結果,我們希望在限定的時間內得到結果,如果得不到,我們會希望取消該任務??傊?,很多情況下我們都有關閉一個線程的需求,那么如何正確的關閉一個線程就是我們要研究的事情,這個事情在上一篇文章中已經討論過了,這里不在贅述。Thread.STOP()之類的api會造成一些不可預知的bug,所以很早便DePRecated了,真要糾結為什么請看這邊文章為何不贊成使用 Thread.stop、Thread.suspend 和 Thread.resume?
Thread類定義了如下關于中斷的方法:
| API | 作用 |
|---|---|
public static boolean interrupted | 就是返回對應線程的中斷標志位是否為true返回當前線程的中斷標志位是否為true,但它還有一個重要的副作用,就是清空中斷標志位,也就是說,連續兩次調用interrupted(),第一次返回的結果為true,第二次一般就是false (除非同時又發生了一次中斷)。 |
public boolean isInterrupted() | 就是返回對應線程的中斷標志位是否為true |
public void interrupt() | 表示中斷對應的線程 |
如果線程在運行中,interrupt()只是會設置線程的中斷標志位,沒有任何其它作用。線程應該在運行過程中合適的位置檢查中斷標志位,比如說,如果主體代碼是一個循環,可以在循環開始處進行檢查,如下所示:
123456789101112131415 | 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狀態:
12 | public final void join() throws InterruptedExceptionpublic final void wait() throws InterruptedException |
執行如下方法會進入TIMED_WAITING狀態:
123 | 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,需要注意的是,拋出異常后,中斷標志位會被清空(線程的中斷標志位會由true重置為false,因為線程為了處理異常已經重新處于就緒狀態。),而不是被設置。比如說,執行如下代碼:
1234567891011121314151617 | Thread t = new Thread (){ @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { //exception被捕獲,但是為輸出為false 因為標志位會被清空 System.out.println(isInterrupted()); } } };t.start();try { Thread.sleep(100);} catch (InterruptedException e) {}t.interrupt();//置為true |
InterruptedException是一個受檢異常,線程必須進行處理。我們在異常處理中介紹過,處理異常的基本思路是,如果你知道怎么處理,就進行處理,如果不知道,就應該向上傳遞,通常情況下,你不應該做的是,捕獲異常然后忽略。
捕獲到InterruptedException,通常表示希望結束該線程,線程大概有兩種處理方式:
向上傳遞該異常,這使得該方法也變成了一個可中斷的方法,需要調用者進行處理有些情況,不能向上傳遞異常,比如Thread的run方法,它的聲明是固定的,不能拋出任何受檢異常,這時,應該捕獲異常,進行合適的清理操作,清理后,一般應該調用Thread的interrupt方法設置中斷標志位,使得其他代碼有辦法知道它發生了中斷第一種方式的示例代碼如下:
12345 | //拋出中斷異常,由調用者捕獲public void interruptibleMethod() throws InterruptedException{ // ... 包含wait, join 或 sleep 方法 Thread.sleep(1000);} |
第二種方式的示例代碼如下:
123456789101112131415161718192021222324252627 | public class InterruptWaitingDemo extends Thread { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { // 模擬任務代碼 Thread.sleep(2000); } catch (InterruptedException e) { // ... 清理操作 System.out.println(isInterrupted());//false // 重設中斷標志位為true Thread.currentThread().interrupt(); } } System.out.println(isInterrupted());//true } public static void main(String[] args) { InterruptWaitingDemo thread = new InterruptWaitingDemo(); thread.start(); try { Thread.sleep(100); } catch (InterruptedException e) { } thread.interrupt(); }} |
如果線程在等待鎖,對線程對象調用interrupt()只是會設置線程的中斷標志位,線程依然會處于BLOCKED狀態,也就是說,interrupt()并不能使一個在等待鎖的線程真正”中斷”。我們看段代碼:
1234567891011121314151617181920212223242526 | 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()并不能使一個在等待鎖的線程真正”中斷”。我們看段代碼:
1234567891011121314151617181920212223242526272829 | public class InterruptSynchronizedDemo { private static Object lock = new Object();//monitor private static class A extends Thread { @Override public void run() { //等待lock鎖 synchronized (lock) { //等待標志位被置為true 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在等待lock鎖,interrupt 無法中斷 a.interrupt(); //a線程加入當前線程,等待執行完畢 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,即變為:
123456789 | public static void test() throws InterruptedException { synchronized (lock) { A a = new A(); a.start(); Thread.sleep(1000); a.interrupt(); } //lock鎖釋放后 A線程重隊列中出來} |
這時,程序就會退出。為什么呢?因為主線程不再等待線程a結束,釋放鎖lock后,線程a會獲得鎖,然后檢測到發生了中斷,所以會退出。
在使用synchronized關鍵字獲取鎖的過程中不響應中斷請求,這是synchronized的局限性。如果這對程序是一個問題,應該使用顯式鎖,java中的Lock接口,它支持以響應中斷的方式獲取鎖。對于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中斷的加鎖操作,它可以拋出中斷異常。等同于等待時間無限長的Lock.tryLock(long time, TimeUnit unit)。
如果線程尚未啟動(NEW),或者已經結束(TERMINATED),則調用interrupt()對它沒有任何效果,中斷標志位也不會被設置。比如說,以下代碼的輸出都是false。
12345678910111213141516171819202122 | 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,則會有一些特殊的處理,我們沒有介紹過網絡,這里只是簡單介紹下。
實現此InterruptibleChannel接口的通道是可中斷的:如果某個線程在可中斷通道上因調用某個阻塞的 I/O 操作(常見的操作一般有這些:serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write)而進入阻塞狀態,而另一個線程又調用了該阻塞線程的 interrupt 方法,這將導致該通道被關閉,并且已阻塞線程接將會收到ClosedByInterruptException,并且設置已阻塞線程的中斷狀態。另外,如果已設置某個線程的中斷狀態并且它在通道上調用某個阻塞的 I/O 操作,則該通道將關閉并且該線程立即接收到 ClosedByInterruptException;并仍然設置其中斷狀態。如果線程阻塞于Selector調用,則線程的中斷標志位會被設置,同時,阻塞的調用會立即返回。我們重點介紹另一種情況,InputStream的read調用,該操作是不可中斷的,如果流中沒有數據,read會阻塞 (但線程狀態依然是RUNNABLE),且不響應interrupt(),與synchronized類似,調用interrupt()只會設置線程的中斷標志,而不會真正”中斷”它,我們看段代碼
1234567891011121314151617181920212223 | public class InterruptReadDemo { private static class A extends Thread { @Override public void run() { while(!Thread.currentThread().isInterrupted()){ try { System.out.println(System.in.read())//wait input } 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方法,我們將代碼改為:
12345678910111213141516171819202122232425262728 | 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方法,這次,程序會輸出:
12 | -1exit |
也就是說,調用close方法后,read方法會返回,返回值為-1,表示流結束。
再比如,ExecutorService提供了如下兩個關閉方法:
12 | void shutdown();List<Runnable> shutdownNow(); |
Future和ExecutorService的API文檔對這些方法都進行了詳細說明,這是我們應該學習的方式。
新聞熱點
疑難解答