1. 引言當(dāng)我們點擊某個殺毒軟件的取消按鈕來停止查殺病毒時,當(dāng)我們在控制臺敲入quit命令以結(jié)束某個后臺服務(wù)時……都需要通過一個線程去取消另一個線程正在執(zhí)行的任務(wù)。Java沒有提供一種安全直接的方法來停止某個線程,但是Java提供了中斷機制。 如果對Java中斷沒有一個全面的了解,可能會誤以為被中斷的線程將立馬退出運行,但事實并非如此。中斷機制是如何工作的?捕獲或檢測到中斷后,是拋出InterruptedException還是重設(shè)中斷狀態(tài)以及在方法中吞掉中斷狀態(tài)會有什么后果?Thread.stop與中斷相比又有哪些異同?什么情況下需要使用中斷?本文將從以上幾個方面進(jìn)行描述。 2. 中斷的原理Java中斷機制是一種協(xié)作機制,也就是說通過中斷并不能直接終止另一個線程,而需要被中斷的線程自己處理中斷。這好比是家里的父母叮囑在外的子女要注意身體,但子女是否注意身體,怎么注意身體則完全取決于自己。 java.lang.Thread類提供了幾個方法來操作這個中斷狀態(tài),這些方法包括:
其中,interrupt方法是唯一能將中斷狀態(tài)設(shè)置為true的方法。靜態(tài)方法interrupted會將當(dāng)前線程的中斷狀態(tài)清除,但這個方法的命名極不直觀,很容易造成誤解,需要特別注意。 上面的例子中,線程t1通過調(diào)用interrupt方法將線程t2的中斷狀態(tài)置為true,t2可以在合適的時候調(diào)用interrupted或isInterrupted來檢測狀態(tài)并做相應(yīng)的處理。 此外,類庫中的有些類的方法也可能會調(diào)用中斷,如FutureTask中的cancel方法,如果傳入的參數(shù)為true,它將會在正在運行異步任務(wù)的線程上調(diào)用interrupt方法,如果正在執(zhí)行的異步任務(wù)中的代碼沒有對中斷做出響應(yīng),那么cancel方法中的參數(shù)將不會起到什么效果;又如ThreadPoolExecutor中的shutdownNow方法會遍歷線程池中的工作線程并調(diào)用線程的interrupt方法來中斷線程,所以如果工作線程中正在執(zhí)行的任務(wù)沒有對中斷做出響應(yīng),任務(wù)將一直執(zhí)行直到正常結(jié)束。 3. 中斷的處理既然Java中斷機制只是設(shè)置被中斷線程的中斷狀態(tài),那么被中斷線程該做些什么? 處理時機 顯然,作為一種協(xié)作機制,不會強求被中斷線程一定要在某個點進(jìn)行處理。實際上,被中斷線程只需在合適的時候處理即可,如果沒有合適的時間點,甚至可以不處理,這時候在任務(wù)處理層面,就跟沒有調(diào)用中斷方法一樣。“合適的時候”與線程正在處理的業(yè)務(wù)邏輯緊密相關(guān),例如,每次迭代的時候,進(jìn)入一個可能阻塞且無法中斷的方法之前等,但多半不會出現(xiàn)在某個臨界區(qū)更新另一個對象狀態(tài)的時候,因為這可能會導(dǎo)致對象處于不一致狀態(tài)。 處理時機決定著程序的效率與中斷響應(yīng)的靈敏性。頻繁的檢查中斷狀態(tài)可能會使程序執(zhí)行效率下降,相反,檢查的較少可能使中斷請求得不到及時響應(yīng)。如果發(fā)出中斷請求之后,被中斷的線程繼續(xù)執(zhí)行一段時間不會給系統(tǒng)帶來災(zāi)難,那么就可以將中斷處理放到方便檢查中斷,同時又能從一定程度上保證響應(yīng)靈敏度的地方。當(dāng)程序的性能指標(biāo)比較關(guān)鍵時,可能需要建立一個測試模型來分析最佳的中斷檢測點,以平衡性能和響應(yīng)靈敏性。 處理方式 1、 中斷狀態(tài)的管理 一般說來,當(dāng)可能阻塞的方法聲明中有拋出InterruptedException則暗示該方法是可中斷的,如BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep等,如果程序捕獲到這些可中斷的阻塞方法拋出的InterruptedException或檢測到中斷后,這些中斷信息該如何處理?一般有以下兩個通用原則:
一般的代碼中,尤其是作為一個基礎(chǔ)類庫時,絕不應(yīng)當(dāng)吞掉中斷,即捕獲到InterruptedException后在catch里什么也不做,清除中斷狀態(tài)后又不重設(shè)中斷狀態(tài)也不拋出InterruptedException等。因為吞掉中斷狀態(tài)會導(dǎo)致方法調(diào)用棧的上層得不到這些信息。 當(dāng)然,凡事總有例外的時候,當(dāng)你完全清楚自己的方法會被誰調(diào)用,而調(diào)用者也不會因為中斷被吞掉了而遇到麻煩,就可以這么做。 總得來說,就是要讓方法調(diào)用棧的上層獲知中斷的發(fā)生。假設(shè)你寫了一個類庫,類庫里有個方法amethod,在amethod中檢測并清除了中斷狀態(tài),而沒有拋出InterruptedException,作為amethod的用戶來說,他并不知道里面的細(xì)節(jié),如果用戶在調(diào)用amethod后也要使用中斷來做些事情,那么在調(diào)用amethod之后他將永遠(yuǎn)也檢測不到中斷了,因為中斷信息已經(jīng)被amethod清除掉了。如果作為用戶,遇到這樣有問題的類庫,又不能修改代碼,那該怎么處理?只好在自己的類里設(shè)置一個自己的中斷狀態(tài),在調(diào)用interrupt方法的時候,同時設(shè)置該狀態(tài),這實在是無路可走時才使用的方法。 2、 中斷的響應(yīng) 程序里發(fā)現(xiàn)中斷后該怎么響應(yīng)?這就得視實際情況而定了。有些程序可能一檢測到中斷就立馬將線程終止,有些可能是退出當(dāng)前執(zhí)行的任務(wù),繼續(xù)執(zhí)行下一個任務(wù)……作為一種協(xié)作機制,這要與中斷方協(xié)商好,當(dāng)調(diào)用interrupt會發(fā)生些什么都是事先知道的,如做一些事務(wù)回滾操作,一些清理工作,一些補償操作等。若不確定調(diào)用某個線程的interrupt后該線程會做出什么樣的響應(yīng),那就不應(yīng)當(dāng)中斷該線程。 4. Thread.interrupt VS Thread.stopThread.stop方法已經(jīng)不推薦使用了。而在某些方面Thread.stop與中斷機制有著相似之處。如當(dāng)線程在等待內(nèi)置鎖或IO時,stop跟interrupt一樣,不會中止這些操作;當(dāng)catch住stop導(dǎo)致的異常時,程序也可以繼續(xù)執(zhí)行,雖然stop本意是要停止線程,這么做會讓程序行為變得更加混亂。 那么它們的區(qū)別在哪里?最重要的就是中斷需要程序自己去檢測然后做相應(yīng)的處理,而Thread.stop會直接在代碼執(zhí)行過程中拋出ThreadDeath錯誤,這是一個java.lang.Error的子類。 在繼續(xù)之前,先來看個小例子: 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里面做了一個非常耗時的排序操作,排序方法中,只有簡單的加、減、賦值、比較等操作,一個可能的執(zhí)行結(jié)果如下: 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方法是個非常耗時的操作,也就是說主線程休眠一秒鐘后調(diào)用stop的時候,線程t還在執(zhí)行sort方法。就是這樣一個簡單的方法,也會拋出錯誤!換一句話說,調(diào)用stop后,大部分Java字節(jié)碼都有可能拋出錯誤,哪怕是簡單的加法! 如果線程當(dāng)前正持有鎖,stop之后則會釋放該鎖。由于此錯誤可能出現(xiàn)在很多地方,那么這就讓編程人員防不勝防,極易造成對象狀態(tài)的不一致。例如,對象obj中存放著一個范圍值:最小值low,最大值high,且low不得大于high,這種關(guān)系由鎖lock保護(hù),以避免并發(fā)時產(chǎn)生競態(tài)條件而導(dǎo)致該關(guān)系失效。假設(shè)當(dāng)前l(fā)ow值是5,high值是10,當(dāng)線程t獲取lock后,將low值更新為了15,此時被stop了,真是糟糕,如果沒有捕獲住stop導(dǎo)致的Error,low的值就為15,high還是10,這導(dǎo)致它們之間的小于關(guān)系得不到保證,也就是對象狀態(tài)被破壞了!如果在給low賦值的時候catch住stop導(dǎo)致的Error則可能使后面high變量的賦值繼續(xù),但是誰也不知道Error會在哪條語句拋出,如果對象狀態(tài)之間的關(guān)系更復(fù)雜呢?這種方式幾乎是無法維護(hù)的,太復(fù)雜了!如果是中斷操作,它決計不會在執(zhí)行l(wèi)ow賦值的時候拋出錯誤,這樣程序?qū)τ趯ο鬆顟B(tài)一致性就是可控的。 正是因為可能導(dǎo)致對象狀態(tài)不一致,stop才被禁用。 5. 中斷的使用通常,中斷的使用場景有以下幾個:
下面來看一個具體的例子。這個例子里,本打算采用GUI形式,但考慮到GUI代碼會使程序復(fù)雜化,就使用控制臺來模擬下核心的邏輯。這里新建了一個磁盤文件掃描的任務(wù),掃描某個目錄下的所有文件并將文件路徑打印到控制臺,掃描的過程可能會很長。若需要中止該任務(wù),只需在控制臺鍵入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("文件掃描任務(wù)被中斷");
}
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();
}
}
在掃描文件的過程中,對于中斷的檢測這里采用的策略是,如果碰到的是文件就不檢測中斷,是目錄才檢測中斷,因為文件可能是非常多的,每次遇到文件都檢測一次會降低程序執(zhí)行效率。此外,在fileIteratorThread線程中,僅是捕獲了InterruptedException,沒有重設(shè)中斷狀態(tài)也沒有繼續(xù)拋出異常,因為我非常清楚它的使用環(huán)境,run方法的調(diào)用棧上層已經(jīng)沒有可能需要檢測中斷狀態(tài)的方法了。 在這個程序中,輸入quit完全可以執(zhí)行System.exit(0)操作來退出程序,但正如前面提到的,這是個GUI程序核心邏輯的模擬,在GUI中,執(zhí)行System.exit(0)會使得整個程序退出。 6. 參考資料
|
|
|