當前位置:才華齋>計算機>java語言>

淺談Java執行緒中斷的本質深入理解

java語言 閱讀(9.35K)

一、Java中斷的現象

淺談Java執行緒中斷的本質深入理解

首先,看看Thread類裡的幾個方法:

public static booleaninterrupted測試當前執行緒是否已經中斷。執行緒的中斷狀態由該方法清除。換句話說,如果連續兩次呼叫該方法,則第二次呼叫將返回 false(在第一次呼叫已清除了其中斷狀態之後,且第二次呼叫檢驗完中斷狀態前,當前執行緒再次中斷的情況除外)。public booleanisInterrupted()測試執行緒是否已經中斷。執行緒的中斷狀態不受該方法的影響。public voidinterrupt()中斷執行緒。

上面列出了與中斷有關的幾個方法及其行為,可以看到interrupt是中斷執行緒。如果不瞭解Java的中斷機制,這樣的一種解釋極容易造成誤解,認為呼叫了執行緒的interrupt方法就一定會中斷執行緒。

其實,Java的中斷是一種協作機制。也就是說呼叫執行緒物件的interrupt方法並不一定就中斷了正在執行的執行緒,它只是要求執行緒自己在合適的時機中斷自己。每個執行緒都有一個boolean的中斷狀態(不一定就是物件的屬性,事實上,該狀態也確實不是Thread的欄位),interrupt方法僅僅只是將該狀態置為true

複製程式碼 程式碼如下:

public class TestInterrupt {

public static void main(String[] args) {

Thread t = new MyThread();

t();

rrupt();

tln("已呼叫執行緒的interrupt方法");

}

static class MyThread extends Thread {

public void run() {

int num = longTimeRunningNonInterruptMethod(2, 0);

tln("長時間任務執行結束,num=" + num);

tln("執行緒的中斷狀態:" + rrupted());

}

private static int longTimeRunningNonInterruptMethod(int count, int initNum) {

for(int i=0; i<count; i++) {

for(int j=0; j<_VALUE; j++) {

initNum ++;

}

}

return initNum;

}

}

}

一般情況下,會列印如下內容:

已呼叫執行緒的interrupt方法

長時間任務執行結束,num=-2

執行緒的中斷狀態:true

可見,interrupt方法並不一定能中斷執行緒。但是,如果改成下面的程式,情況會怎樣呢?

複製程式碼 程式碼如下:

import Unit;

public class TestInterrupt {

public static void main(String[] args) {

Thread t = new MyThread();

t();

rrupt();

tln("已呼叫執行緒的interrupt方法");

}

static class MyThread extends Thread {

public void run() {

int num = -1;

try {

num = longTimeRunningInterruptMethod(2, 0);

} catch (InterruptedException e) {

tln("執行緒被中斷");

throw new RuntimeException(e);

}

tln("長時間任務執行結束,num=" + num);

tln("執行緒的中斷狀態:" + rrupted());

}

private static int longTimeRunningInterruptMethod(int count, int initNum) throws InterruptedException{

for(int i=0; i<count; i++) {

p(5);

}

return initNum;

}

}

}

經執行可以發現,程式丟擲異常停止了,run方法裡的後兩條列印語句沒有執行。那麼,區別在哪裡?

一般說來,如果一個方法宣告丟擲InterruptedException,表示該方法是可中斷的(沒有在方法中處理中斷卻也宣告丟擲InterruptedException的除外),也就是說可中斷方法會對interrupt呼叫做出響應(例如sleep響應interrupt的操作包括清除中斷狀態,丟擲InterruptedException),如果interrupt呼叫是在可中斷方法之前呼叫,可中斷方法一定會處理中斷,像上面的例子,interrupt方法極可能在run未進入sleep的時候就呼叫了,但sleep檢測到中斷,就會處理該中斷。如果在可中斷方法正在執行中的時候呼叫interrupt,會怎麼樣呢?這就要看可中斷方法處理中斷的時機了,只要可中斷方法能檢測到中斷狀態為true,就應該處理中斷。讓我們為開頭的那段程式碼加上中斷處理。

那麼自定義的可中斷方法該如何處理中斷呢?那就是在適合處理中斷的地方檢測執行緒中斷狀態並處理。

複製程式碼 程式碼如下:

public class TestInterrupt {

public static void main(String[] args) throws Exception {

Thread t = new MyThread();

t();

// p(1);//如果不能看到處理過程中被中斷的情形,可以啟用這句再看看效果

rrupt();

tln("已呼叫執行緒的interrupt方法");

}

static class MyThread extends Thread {

public void run() {

int num;

try {

num = longTimeRunningNonInterruptMethod(2, 0);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

tln("長時間任務執行結束,num=" + num);

tln("執行緒的中斷狀態:" + rrupted());

}

private static int longTimeRunningNonInterruptMethod(int count, int initNum) throws InterruptedException {

if(interrupted()) {

throw new InterruptedException("正式處理前執行緒已經被請求中斷");

}

for(int i=0; i<count; i++) {

for(int j=0; j<_VALUE; j++) {

initNum ++;

}

//假如這就是一個合適的地方

if(interrupted()) {

//回滾資料,清理操作等

throw new InterruptedException("執行緒正在處理過程中被中斷");

}

}

return initNum;

}

}

}

如上面的程式碼,方法longTimeRunningMethod此時已是一個可中斷的方法了。在進入方法的時候判斷是否被請求中斷,如果是,就不進行相應的處理了;處理過程中,可能也有合適的地方處理中斷,例如上面最內層迴圈結束後。

這段程式碼中檢測中斷用了Thread的靜態方法interrupted,它將中斷狀態置為false,並將之前的狀態返回,而isInterrupted只是檢測中斷,並不改變中斷狀態。一般來說,處理過了中斷請求,應該將其狀態置為false。但具體還要看實際情形。

二、Java中斷的本質

歷史上,Java試圖提供過搶佔式限制中斷,但問題多多,例如已被廢棄的、end和 me等。另一方面,出於Java應用程式碼的健壯性的考慮,降低了程式設計門檻,減少不清楚底層機制的程式設計師無意破壞系統的概率。

如今,Java的執行緒排程不提供搶佔式中斷,而採用協作式的中斷。其實,協作式的中斷,原理很簡單,就是輪詢某個表示中斷的標記,我們在任何普通程式碼的中都可以實現。 例如下面的程式碼:

複製程式碼 程式碼如下:

volatile bool isInterrupted;

//…

while(!isInterrupted) {

compute();

}

但是,上述的程式碼問題也很明顯。當compute執行時間比較長時,中斷無法及時被響應。另一方面,利用輪詢檢查標誌變數的方式,想要中斷wait和sleep等執行緒阻塞操作也束手無策。

如果仍然利用上面的思路,要想讓中斷及時被響應,必須在虛擬機器底層進行執行緒排程的對標記變數進行檢查。是的,JVM中確實是這樣做的。下面摘自ad的原始碼:

複製程式碼 程式碼如下:

public static boolean interrupted() {

return currentThread()terrupted(true);

}

//…

private native boolean isInterrupted(boolean ClearInterrupted);

可以發現,isInterrupted被宣告為native方法,取決於JVM底層的實現。

實際上,JVM內部確實為每個執行緒維護了一箇中斷標記。但應用程式不能直接訪問這個中斷變數,必須通過下面幾個方法進行操作:

複製程式碼 程式碼如下:

public class Thread {

//設定中斷標記

public void interrupt() { ... }

//獲取中斷標記的值

public boolean isInterrupted() { ... }

//清除中斷標記,並返回上一次中斷標記的值

public static boolean interrupted() { ... }

...

}

通常情況下,呼叫執行緒的interrupt方法,並不能立即引發中斷,只是設定了JVM內部的中斷標記。因此,通過檢查中斷標記,應用程式可以做一些特殊操作,也可以完全忽略中斷。

你可能想,如果JVM只提供了這種簡陋的`中斷機制,那和應用程式自己定義中斷變數並輪詢的方法相比,基本也沒有什麼優勢。

JVM內部中斷變數的主要優勢,就是對於某些情況,提供了模擬自動“中斷陷入”的機制。

在執行涉及執行緒排程的阻塞呼叫時(例如wait、sleep和join),如果發生中斷,被阻塞執行緒會“儘可能快的”丟擲InterruptedException。因此,我們就可以用下面的程式碼框架來處理執行緒阻塞中斷:

複製程式碼 程式碼如下:

try {

//wait、sleep或join

}

catch(InterruptedException e) {

//某些中斷處理工作

}

所謂“儘可能快”,我猜測JVM就是線上程排程排程的間隙檢查中斷變數,速度取決於JVM的實現和硬體的效能。

三、一些不會丟擲 InterruptedException 的執行緒阻塞操作

然而,對於某些執行緒阻塞操作,JVM並不會自動丟擲InterruptedException異常。例如,某些I/O操作和內部鎖操作。對於這類操作,可以用其他方式模擬中斷:

1)中的非同步socket I/O

讀寫socket的時候,InputStream和OutputStream的read和write方法會阻塞等待,但不會響應java中斷。不過,呼叫Socket的close方法後,被阻塞執行緒會丟擲SocketException異常。

2)利用Selector實現的非同步I/O

如果執行緒被阻塞於ct(在nels中),呼叫wakeup方法會引起ClosedSelectorException異常。

3)鎖獲取

如果執行緒在等待獲取一個內部鎖,我們將無法中斷它。但是,利用Lock類的lockInterruptibly方法,我們可以在等待鎖的同時,提供中斷能力。

四、兩條程式設計原則

另外,在任務與執行緒分離的框架中,任務通常並不知道自身會被哪個執行緒呼叫,也就不知道呼叫執行緒處理中斷的策略。所以,在任務設定了執行緒中斷標記後,並不能確保任務會被取消。因此,有以下兩條程式設計原則:

1)除非你知道執行緒的中斷策略,否則不應該中斷它。

這條原則告訴我們,不應該直接呼叫Executer之類框架中執行緒的interrupt方法,應該利用諸如el的方法來取消任務。

2)任務程式碼不該猜測中斷對執行執行緒的含義。

這條原則告訴我們,一般程式碼遇在到InterruptedException異常時,不應該將其捕獲後“吞掉”,而應該繼續向上層程式碼丟擲。

總之,Java中的非搶佔式中斷機制,要求我們必須改變傳統的搶佔式中斷思路,在理解其本質的基礎上,採用相應的原則和模式來程式設計。