當咱們點擊某個殺毒軟件的取消按鈕來中止查殺病毒時,當咱們在控制檯敲入quit命令以結束某個後臺服務時……都須要經過一個線程去取消另外一個線程正在執行的任務。Java沒有提供一種安全直接的方法來中止某個線程,可是Java提供了中斷機制。java
若是對Java中斷沒有一個全面的瞭解,可能會誤覺得被中斷的線程將立馬退出運行,但事實並不是如此。中斷機制是如何工做的?捕獲或檢測到中斷後,是拋出InterruptedException仍是重設中斷狀態以及在方法中吞掉中斷狀態會有什麼後果?Thread.stop與中斷相比又有哪些異同?什麼狀況下須要使用中斷?本文將從以上幾個方面進行描述。編程
Java中斷機制是一種協做機制,也就是說經過中斷並不能直接終止另外一個線程,而須要被中斷的線程本身處理中斷。這比如是家裏的父母叮囑在外的子女要注意身體,但子女是否注意身體,怎麼注意身體則徹底取決於本身。安全
Java中斷模型也是這麼簡單,每一個線程對象裏都有一個boolean類型的標識(不必定就要是Thread類的字段,實際上也的確不是,這幾個方法最終都是經過native方法來完成的),表明着是否有中斷請求(該請求能夠來自全部線程,包括被中斷的線程自己)。例如,當線程t1想中斷線程t2,只須要在線程t1中將線程t2對象的中斷標識置爲true,而後線程2能夠選擇在合適的時候處理該中斷請求,甚至能夠不理會該請求,就像這個線程沒有被中斷同樣。bash
java.lang.Thread類提供了幾個方法來操做這個中斷狀態,這些方法包括:併發
public static boolean interrupted
測試當前線程是否已經中斷。線程的dom
public boolean isInterrupted()
測試線程是否已經中斷。線程的異步
public void interrupt()
中斷線程。性能
其中,interrupt方法是惟一能將中斷狀態設置爲true的方法。靜態方法interrupted會將當前線程的中斷狀態清除,但這個方法的命名極不直觀,很容易形成誤解,須要特別注意。學習
上面的例子中,線程t1經過調用interrupt方法將線程t2的中斷狀態置爲true,t2能夠在合適的時候調用interrupted或isInterrupted來檢測狀態並作相應的處理。測試
此外,類庫中的有些類的方法也可能會調用中斷,如FutureTask中的cancel方法,若是傳入的參數爲true,它將會在正在運行異步任務的線程上調用interrupt方法,若是正在執行的異步任務中的代碼沒有對中斷作出響應,那麼cancel方法中的參數將不會起到什麼效果;又如ThreadPoolExecutor中的shutdownNow方法會遍歷線程池中的工做線程並調用線程的interrupt方法來中斷線程,因此若是工做線程中正在執行的任務沒有對中斷作出響應,任務將一直執行直到正常結束。
既然Java中斷機制只是設置被中斷線程的中斷狀態,那麼被中斷線程該作些什麼?
顯然,做爲一種協做機制,不會強求被中斷線程必定要在某個點進行處理。實際上,被中斷線程只需在合適的時候處理便可,若是沒有合適的時間點,甚至能夠不處理,這時候在任務處理層面,就跟沒有調用中斷方法同樣。「合適的時候」與線程正在處理的業務邏輯緊密相關,例如,每次迭代的時候,進入一個可能阻塞且沒法中斷的方法以前等,但多半不會出如今某個臨界區更新另外一個對象狀態的時候,由於這可能會致使對象處於不一致狀態。
處理時機決定着程序的效率與中斷響應的靈敏性。頻繁的檢查中斷狀態可能會使程序執行效率降低,相反,檢查的較少可能使中斷請求得不到及時響應。若是發出中斷請求以後,被中斷的線程繼續執行一段時間不會給系統帶來災難,那麼就能夠將中斷處理放到方便檢查中斷,同時又能從必定程度上保證響應靈敏度的地方。當程序的性能指標比較關鍵時,可能須要創建一個測試模型來分析最佳的中斷檢測點,以平衡性能和響應靈敏性。
一、 中斷狀態的管理
通常說來,當可能阻塞的方法聲明中有拋出InterruptedException則暗示該方法是可中斷的,如BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep等,若是程序捕獲到這些可中斷的阻塞方法拋出的InterruptedException或檢測到中斷後,這些中斷信息該如何處理?通常有如下兩個通用原則:
通常的代碼中,尤爲是做爲一個基礎類庫時,毫不應當吞掉中斷,即捕獲到InterruptedException後在catch裏什麼也不作,清除中斷狀態後又不重設中斷狀態也不拋出InterruptedException等。由於吞掉中斷狀態會致使方法調用棧的上層得不到這些信息。
固然,凡事總有例外的時候,當你徹底清楚本身的方法會被誰調用,而調用者也不會由於中斷被吞掉了而遇到麻煩,就能夠這麼作。
總得來講,就是要讓方法調用棧的上層獲知中斷的發生。假設你寫了一個類庫,類庫裏有個方法amethod,在amethod中檢測並清除了中斷狀態,而沒有拋出InterruptedException,做爲amethod的用戶來講,他並不知道里面的細節,若是用戶在調用amethod後也要使用中斷來作些事情,那麼在調用amethod以後他將永遠也檢測不到中斷了,由於中斷信息已經被amethod清除掉了。若是做爲用戶,遇到這樣有問題的類庫,又不能修改代碼,那該怎麼處理?只好在本身的類裏設置一個本身的中斷狀態,在調用interrupt方法的時候,同時設置該狀態,這實在是無路可走時才使用的方法。
二、 中斷的響應
程序裏發現中斷後該怎麼響應?這就得視實際狀況而定了。有些程序可能一檢測到中斷就立馬將線程終止,有些多是退出當前執行的任務,繼續執行下一個任務……做爲一種協做機制,這要與中斷方協商好,當調用interrupt會發生些什麼都是事先知道的,如作一些事務回滾操做,一些清理工做,一些補償操做等。若不肯定調用某個線程的interrupt後該線程會作出什麼樣的響應,那就不該當中斷該線程。
Thread.stop方法已經不推薦使用了。而在某些方面Thread.stop與中斷機制有着類似之處。如當線程在等待內置鎖或IO時,stop跟interrupt同樣,不會停止這些操做;當catch住stop致使的異常時,程序也能夠繼續執行,雖然stop本意是要中止線程,這麼作會讓程序行爲變得更加混亂。
那麼它們的區別在哪裏?最重要的就是中斷須要程序本身去檢測而後作相應的處理,而Thread.stop會直接在代碼執行過程當中拋出ThreadDeath錯誤,這是一個java.lang.Error的子類。
在繼續以前,先來看個小例子:
package com.ticmy.interrupt;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class TestStop {
private static final int[] array = new int[80000];
private static final Thread t = new Thread() {
public void run() {
try {
System.out.println(sort(array));
} catch (Error err) {
err.printStackTrace();
}
System.out.println("in thread t");
}
};
static {
Random random = new Random();
for(int i = 0; i < array.length; i++) {
array[i] = random.nextInt(i + 1);
}
}
private static int sort(int[] array) {
for (int i = 0; i < array.length-1; i++){
for(int j = 0 ;j < array.length - i - 1; j++){
if(array[j] < array[j + 1]){
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
return array[0];
}
public static void main(String[] args) throws Exception {
t.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("go to stop thread t");
t.stop();
System.out.println("finish main");
}
}複製代碼
這個例子很簡單,線程t裏面作了一個很是耗時的排序操做,排序方法中,只有簡單的加、減、賦值、比較等操做,一個可能的執行結果以下:
go to stop thread t
java.lang.ThreadDeath
at java.lang.Thread.stop(Thread.java:758)
at com.ticmy.interrupt.TestStop.main(TestStop.java:44)
finish main
in thread t複製代碼
這裏sort方法是個很是耗時的操做,也就是說主線程休眠一秒鐘後調用stop的時候,線程t還在執行sort方法。就是這樣一個簡單的方法,也會拋出錯誤!換一句話說,調用stop後,大部分Java字節碼都有可能拋出錯誤,哪怕是簡單的加法!
若是線程當前正持有鎖,stop以後則會釋放該鎖。因爲此錯誤可能出如今不少地方,那麼這就讓編程人員防不勝防,極易形成對象狀態的不一致。例如,對象obj中存放着一個範圍值:最小值low,最大值high,且low不得大於high,這種關係由鎖lock保護,以免併發時產生競態條件而致使該關係失效。假設當前low值是5,high值是10,當線程t獲取lock後,將low值更新爲了15,此時被stop了,真是糟糕,若是沒有捕獲住stop致使的Error,low的值就爲15,high仍是10,這致使它們之間的小於關係得不到保證,也就是對象狀態被破壞了!若是在給low賦值的時候catch住stop致使的Error則可能使後面high變量的賦值繼續,可是誰也不知道Error會在哪條語句拋出,若是對象狀態之間的關係更復雜呢?這種方式幾乎是沒法維護的,太複雜了!若是是中斷操做,它決計不會在執行low賦值的時候拋出錯誤,這樣程序對於對象狀態一致性就是可控的。
正是由於可能致使對象狀態不一致,stop才被禁用。
一般,中斷的使用場景有如下幾個:
下面來看一個具體的例子。這個例子裏,本打算採用GUI形式,但考慮到GUI代碼會使程序複雜化,就使用控制檯來模擬下核心的邏輯。這裏新建了一個磁盤文件掃描的任務,掃描某個目錄下的全部文件並將文件路徑打印到控制檯,掃描的過程可能會很長。若須要停止該任務,只需在控制檯鍵入quit並回車便可。
package com.ticmy.interrupt;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
public class FileScanner {
private static void listFile(File f) throws InterruptedException {
if(f == null) {
throw new IllegalArgumentException();
}
if(f.isFile()) {
System.out.println(f);
return;
}
File[] allFiles = f.listFiles();
if(Thread.interrupted()) {
throw new InterruptedException("文件掃描任務被中斷");
}
for(File file : allFiles) {
//還能夠將中斷檢測放到這裏
listFile(file);
}
}
public static String readFromConsole() {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
return reader.readLine();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
public static void main(String[] args) throws Exception {
final Thread fileIteratorThread = new Thread() {
public void run() {
try {
listFile(new File("c:\\"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread() {
public void run() {
while(true) {
if("quit".equalsIgnoreCase(readFromConsole())) {
if(fileIteratorThread.isAlive()) {
fileIteratorThread.interrupt();
return;
}
} else {
System.out.println("輸入quit退出文件掃描");
}
}
}
}.start();
fileIteratorThread.start();
}
}複製代碼
今天的技術分享就到這裏,歡迎你們掃下方二維碼加學習技術交流羣,一塊兒夯實基礎,提高自我價值。