497
阿裏雲
技術社區[雲棲]
詳細分析Java中斷機製
1. 引言
當我們點擊某個殺毒軟件的取消按鈕來停止查殺病毒時,當我們在控製台敲入quit命令以結束某個後台服務時……都需要通過一個線程去取消另一個線程正在執行的任務。Java沒有提供一種安全直接的方法來停止某個線程,但是Java提供了中斷機製。
如果對Java中斷沒有一個全麵的了解,可能會誤以為被中斷的線程將立馬退出運行,但事實並非如此。中斷機製是如何工作的?捕獲或檢測到中斷後,是拋出
InterruptedException還是重設中斷狀態以及在方法中吞掉中斷狀態會有什麼後果?Thread.stop與中斷相比又有哪些異同?什麼
情況下需要使用中斷?本文將從以上幾個方麵進行描述。
2. 中斷的原理
Java中斷機製是一種協作機製,也就是說通過中斷並不能直接終止另一個線程,而需要被中斷的線程自己處理中斷。這好比是家裏的父母叮囑在外的子女要注意身體,但子女是否注意身體,怎麼注意身體則完全取決於自己。
Java中斷模型也是這麼簡單,每個線程對象裏都有一個boolean類型的標識(不一定就要是Thread類的字段,實際上也的確不是,這幾個方法最終
都是通過native方法來完成的),代表著是否有中斷請求(該請求可以來自所有線程,包括被中斷的線程本身)。例如,當線程t1想中斷線程t2,隻需要
在線程t1中將線程t2對象的中斷標識置為true,然後線程2可以選擇在合適的時候處理該中斷請求,甚至可以不理會該請求,就像這個線程沒有被中斷一
樣。
java.lang.Thread類提供了幾個方法來操作這個中斷狀態,這些方法包括:
public static boolean interrupted
|
測試當前線程是否已經中斷。線程的中斷狀態 由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回 false(在第一次調用已清除了其中斷狀態之後,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。
|
public boolean isInterrupted()
|
測試線程是否已經中斷。線程的中斷狀態不受該方法的影響。
|
public void interrupt()
|
中斷線程。
|
其中,interrupt方法是唯一能將中斷狀態設置為true的方法。靜態方法interrupted會將當前線程的中斷狀態清除,但這個方法的命名極不直觀,很容易造成誤解,需要特別注意。
上麵的例子中,線程t1通過調用interrupt方法將線程t2的中斷狀態置為true,t2可以在合適的時候調用interrupted或isInterrupted來檢測狀態並做相應的處理。
此外,類庫中的有些類的方法也可能會調用中斷,如FutureTask中的cancel方法,如果傳入的參數為true,它將會在正在運行異步任務的線程
上調用interrupt方法,如果正在執行的異步任務中的代碼沒有對中斷做出響應,那麼cancel方法中的參數將不會起到什麼效果;又如
ThreadPoolExecutor中的shutdownNow方法會遍曆線程池中的工作線程並調用線程的interrupt方法來中斷線程,所以如果
工作線程中正在執行的任務沒有對中斷做出響應,任務將一直執行直到正常結束。
3. 中斷的處理
既然Java中斷機製隻是設置被中斷線程的中斷狀態,那麼被中斷線程該做些什麼?
處理時機
顯然,作為一種協作機製,不會強求被中斷線程一定要在某個點進行處理。實際上,被中斷線程隻需在合適的時候處理即可,如果沒有合適的時間點,甚至可以不處
理,這時候在任務處理層麵,就跟沒有調用中斷方法一樣。“合適的時候”與線程正在處理的業務邏輯緊密相關,例如,每次迭代的時候,進入一個可能阻塞且無法
中斷的方法之前等,但多半不會出現在某個臨界區更新另一個對象狀態的時候,因為這可能會導致對象處於不一致狀態。
處理時機決定著程序的效率與中斷響應的靈敏性。頻繁的檢查中斷狀態可能會使程序執行效率下降,相反,檢查的較少可能使中斷請求得不到及時響應。如果發出中
斷請求之後,被中斷的線程繼續執行一段時間不會給係統帶來災難,那麼就可以將中斷處理放到方便檢查中斷,同時又能從一定程度上保證響應靈敏度的地方。當程
序的性能指標比較關鍵時,可能需要建立一個測試模型來分析最佳的中斷檢測點,以平衡性能和響應靈敏性。
處理方式
1、 中斷狀態的管理
一般說來,當可能阻塞的方法聲明中有拋出InterruptedException則暗示該方法是可中斷的,如BlockingQueue#put、
BlockingQueue#take、Object#wait、Thread#sleep等,如果程序捕獲到這些可中斷的阻塞方法拋出的
InterruptedException或檢測到中斷後,這些中斷信息該如何處理?一般有以下兩個通用原則:
-
如果遇到的是可中斷的阻塞方法拋出InterruptedException,可以繼續向方法調用棧的上層拋出該異常,如果是檢測到中斷,則可清除中斷狀態並拋出InterruptedException,使當前方法也成為一個可中斷的方法。
-
若有時候不太方便在方法上拋出InterruptedException,比如要實現的某個接口中的方法簽名上沒有throws
InterruptedException,這時就可以捕獲可中斷方法的InterruptedException並通過
Thread.currentThread.interrupt()來重新設置中斷狀態。如果是檢測並清除了中斷狀態,亦是如此。
一般的代碼中,尤其是作為一個基礎類庫時,絕不應當吞掉中斷,即捕獲到InterruptedException後在catch裏什麼也不做,清除中斷狀
態後又不重設中斷狀態也不拋出InterruptedException等。因為吞掉中斷狀態會導致方法調用棧的上層得不到這些信息。
當然,凡事總有例外的時候,當你完全清楚自己的方法會被誰調用,而調用者也不會因為中斷被吞掉了而遇到麻煩,就可以這麼做。
總得來說,就是要讓方法調用棧的上層獲知中斷的發生。假設你寫了一個類庫,類庫裏有個方法amethod,在amethod中檢測並清除了中斷狀態,而沒
有拋出InterruptedException,作為amethod的用戶來說,他並不知道裏麵的細節,如果用戶在調用amethod後也要使用中斷來
做些事情,那麼在調用amethod之後他將永遠也檢測不到中斷了,因為中斷信息已經被amethod清除掉了。如果作為用戶,遇到這樣有問題的類庫,又
不能修改代碼,那該怎麼處理?隻好在自己的類裏設置一個自己的中斷狀態,在調用interrupt方法的時候,同時設置該狀態,這實在是無路可走時才使用
的方法。
2、 中斷的響應
程序裏發現中斷後該怎麼響應?這就得視實際情況而定了。有些程序可能一檢測到中斷就立馬將線程終止,有些可能是退出當前執行的任務,繼續執行下一個任
務……作為一種協作機製,這要與中斷方協商好,當調用interrupt會發生些什麼都是事先知道的,如做一些事務回滾操作,一些清理工作,一些補償操作
等。若不確定調用某個線程的interrupt後該線程會做出什麼樣的響應,那就不應當中斷該線程。
4. Thread.interrupt VS Thread.stop
Thread.stop方法已經不推薦使用了。而在某些方麵Thread.stop與中斷機製有著相似之處。如當線程在等待內置鎖或IO時,stop跟
interrupt一樣,不會中止這些操作;當catch住stop導致的異常時,程序也可以繼續執行,雖然stop本意是要停止線程,這麼做會讓程序行
為變得更加混亂。
那麼它們的區別在哪裏?最重要的就是中斷需要程序自己去檢測然後做相應的處理,而Thread.stop會直接在代碼執行過程中拋出ThreadDeath錯誤,這是一個java.lang.Error的子類。
在繼續之前,先來看個小例子:
01 |
package com.ticmy.interrupt;
|
02 |
import java.util.Arrays;
|
03 |
import java.util.Random;
|
04 |
import java.util.concurrent.TimeUnit;
|
05 |
public class TestStop {
|
06 |
private static final int [] array = new int [ 80000 ];
|
07 |
private static final Thread t = new Thread() {
|
10 |
System.out.println(sort(array));
|
12 |
err.printStackTrace();
|
14 |
System.out.println( "in thread t" );
|
19 |
Random random = new Random();
|
20 |
for ( int i = 0 ; i < array.length; i++) {
|
21 |
array[i] = random.nextInt(i + 1 );
|
25 |
private static int sort( int [] array) {
|
26 |
for ( int i = 0 ; i < array.length- 1 ; i++){
|
27 |
for ( int j = 0 ;j < array.length - i - 1 ; j++){
|
28 |
if (array[j] < array[j + 1 ]){
|
30 |
array[j] = array[j + 1 ];
|
38 |
public static void main(String[] args) throws Exception {
|
40 |
TimeUnit.SECONDS.sleep( 1 );
|
41 |
System.out.println( "go to stop thread t" );
|
43 |
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才被禁用。
5. 中斷的使用
通常,中斷的使用場景有以下幾個:
- 點擊某個桌麵應用中的取消按鈕時;
- 某個操作超過了一定的執行時間限製需要中止時;
- 多個線程做相同的事情,隻要一個線程成功其它線程都可以取消時;
- 一組線程中的一個或多個出現錯誤導致整組都無法繼續時;
- 當一個應用或服務需要停止時。
下麵來看一個具體的例子。這個例子裏,本打算采用GUI形式,但考慮到GUI代碼會使程序複雜化,就使用控製台來模擬下核心的邏輯。這裏新建了一個磁盤文
件掃描的任務,掃描某個目錄下的所有文件並將文件路徑打印到控製台,掃描的過程可能會很長。若需要中止該任務,隻需在控製台鍵入quit並回車即可。
01 |
package com.ticmy.interrupt;
|
02 |
import java.io.BufferedReader;
|
04 |
import java.io.InputStreamReader;
|
06 |
public class FileScanner {
|
07 |
private static void listFile(File f) throws InterruptedException {
|
09 |
throw new IllegalArgumentException();
|
12 |
System.out.println(f);
|
15 |
File[] allFiles = f.listFiles();
|
16 |
if (Thread.interrupted()) {
|
17 |
throw new InterruptedException( "文件掃描任務被中斷" );
|
19 |
for (File file : allFiles) {
|
25 |
public static String readFromConsole() {
|
26 |
BufferedReader reader = new BufferedReader( new InputStreamReader(System.in));
|
28 |
return reader.readLine();
|
29 |
} catch (Exception e) {
|
35 |
public static void main(String[] args) throws Exception {
|
36 |
final Thread fileIteratorThread = new Thread() {
|
39 |
listFile( new File( "c:\\" ));
|
40 |
} catch (InterruptedException e) {
|
48 |
if ( "quit" .equalsIgnoreCase(readFromConsole())) {
|
49 |
if (fileIteratorThread.isAlive()) {
|
50 |
fileIteratorThread.interrupt();
|
54 |
System.out.println( "輸入quit退出文件掃描" );
|
59 |
fileIteratorThread.start();
|
在掃描文件的過程中,對於中斷的檢測這裏采用的策略是,如果碰到的是文件就不檢測中斷,是目錄才檢測中斷,因為文件可能是非常多的,每次遇到文件都檢測一
次會降低程序執行效率。此外,在fileIteratorThread線程中,僅是捕獲了InterruptedException,沒有重設中斷狀態也
沒有繼續拋出異常,因為我非常清楚它的使用環境,run方法的調用棧上層已經沒有可能需要檢測中斷狀態的方法了。
在這個程序中,輸入quit完全可以執行System.exit(0)操作來退出程序,但正如前麵提到的,這是個GUI程序核心邏輯的模擬,在GUI中,執行System.exit(0)會使得整個程序退出。
6. 參考資料
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-22 17:31:41