本文轉載自:Java Thread.interrupt[點擊打開鏈接], 文章寫的太好,怕丟了,所以原文也一併拷貝過來。
下面這個場景你可能很熟悉,我們調用Thread.sleep()
,condition.await()
,但是IDE提示我們有未捕獲的InterruptedException
。什麼是InterruptedException
呢?我們又應該怎麼處理呢?
大部分人的回答是,吞掉這個異常就好啦。但是其實,這個異常往往帶有重要的信息,可以讓我們具備關閉應用時執行當前代碼回收的能力。
Blocking Method
如果一個方法拋出InterruptedException
(或者類似的),那麼說明這個方法是一個阻塞方法。(非阻塞的方法需要你自己判斷,阻塞方法只有通過異常才能反饋出這個東西)
當我們直接調用一個Unschedule
的線程的interrupt
方法的時候,會立即使得其變成schedule(這一點非常重要,由JVM保證), 並且interrupted
狀態位爲true。
通常low-level method,像sleep和await這些方法就會在方法內部處理這個標誌位。例如await就會在醒來之後檢查是否有中斷。所以在Sync
內部通常在喚醒之後都會檢查中斷標誌位。
看下面一段代碼:
public static void main(String[] args) {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// what to do ?
}
System.out.println(System.currentTimeMillis() - start);
}
});
a.start();
// 加上這句話,執行時間是0,沒有這句話執行時間是10,你感受下
a.interrupt();
}
所以,當我們直接Interrupt一個線程的時候,他會立即變成可調度的狀態,也就是會里面從阻塞函數中返回。這個時候我們拿到InterruptedException(或者在更底層看來只是一個線程中斷標誌位)的時候應該怎麼做呢?
在low-level的層面來說,只有中斷標誌位,這一個概念,並沒有interruptException,只是jdk的框架代碼中,爲了強制讓客戶端處理這種異常,所以在同步器、線程等阻塞方法中喚醒後自動檢測了中斷標誌位,如果符合條件,則直接拋出受檢異常。
How to Deal
Propagating InterruptedException to callers by not catching it
當你的方法調用一個blocking方法的時候,說明你這個方法也是一個blocking方法(大多數情況下)。這個時候你就需要對interruptException有一定的處理策略,通常情況下最簡單的策略是把他拋出去。參考下面的代碼:
參考blockingQueue的寫法,底層使用condition對象,當await喚醒的時候有interruptException的時候,直接拋出,便於上層處理。換句話說,你的代碼這個層面沒有處理的必要和意義。
public class TaskQueue {
private static final int MAX_TASKS = 1000;
private BlockingQueue<Task> queue
= new LinkedBlockingQueue<Task>(MAX_TASKS);
public void putTask(Task r) throws InterruptedException {
queue.put(r);
}
public Task getTask() throws InterruptedException {
return queue.take();
}
}
Do some clean up before thrown out
有時候,在當前的代碼層級上,拋出interruptException需要清理當前的類,清理完成後再把異常拋出去。下面的代碼,表現的就是一個遊戲匹配器的功能,首先等待玩家1,玩家2都到達之後開始遊戲。如果當前玩家1到達了,線程接受到interrupt請求,那麼釋放玩家1,這樣就不會有玩家丟失。
public class PlayerMatcher {
private PlayerSource players;
public PlayerMatcher(PlayerSource players) {
this.players = players;
}
public void matchPlayers() throws InterruptedException {
Player playerOne, playerTwo;
try {
while (true) {
playerOne = playerTwo = null;
// Wait for two players to arrive and start a new game
playerOne = players.waitForPlayer(); // could throw IE
playerTwo = players.waitForPlayer(); // could throw IE
startNewGame(playerOne, playerTwo);
}
}
catch (InterruptedException e) {
// If we got one player and were interrupted, put that player back
if (playerOne != null)
players.addFirst(playerOne);
// Then propagate the exception
throw e;
}
}
}
Resorting Status
如果已經到了拋不出去的地步了,比如在Runnable中。當一個blocking-method拋出一個interruptException的時候,當前線程的中斷標誌位實際是已經被清除了的,如果我們這個時候不能再次拋出interruptException,我們就無法向上層表達中斷的意義。這個時候只有重置中斷狀態。但是,這裏面還是有很多技巧...不要瞎搞:
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
}
注意上面代碼,catch異常的位置,在看下面一段代碼
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
while (true) {
try {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
這段代碼就會造成無限循環,catch住之後,設置中斷標誌,然後loop,
take()
函數立即拋出InterruptException
。你感受下。
千萬不要直接吞掉
當你不能拋出InterruptedException
,不論你決定是否響應interrupt request
,這個時候你都必須重置當前線程的interrupt
標誌位,因爲interrupt
標誌位不是給你一個人看的,還有很多邏輯相應這個狀態。標準的線程池(ThreadPoolExecutor)的Worker對象(內部類)其實也會對interrupt標識位響應,所以向一個task發出中斷信號又兩個作用,1是取消這個任務,2是告訴執行的Thread線程池正在關閉。如果一個task吞掉了中斷請求,worker thread就不能響應中斷請求,這可能導致application一直不能shutdown.
萬不要直接吞掉
// Don't do this
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException swallowed) {
/* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
}
}
}
Implementing cancelable tasks
從來沒有任何文檔給出interruption
明確的語義,但是其實在大型程序中,中斷可能只有一個語義:取消,因爲別的語義實在是難以維持。舉個例子,一個用戶可以用通過GUI程序,或者通過一些網絡機制例如JMX
或者WebService
來發出一個關閉請求。也可能是一段程序邏輯,再舉個簡單的例子,一個爬蟲程序如果檢測到磁盤滿了,可能就會自行發出中斷(取消)請求。或者一個並行算法可能會打開多個線程來搜索什麼東西,當某個框架搜索到結果之後,就會發出中斷(取消)請求。
一個task is cancelable並不意味着他必須立刻響應中斷請求。如果一個task在loop中執行,一個典型的寫法是在每次Loop中都檢查中斷標誌位。可能循環時間會對響應時間造成一定的delay。你可以通過一些寫法來提高中斷的響應速度,例如blocking method裏面往往第一行都是檢查中斷標誌位。
Interrupts can be swallowed if you know the thread is about to exit 唯一可以吞掉interruptException的場景是,你明確知道線程就要退出了。這個場景往往出現在,調用中斷方法是在你的類的內部,例如下面一段代碼,而不是被某種框架中斷。
public class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted())
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
/* Allow thread to exit */
}
}
public void cancel() { interrupt(); }
}
Non-Interruptible Blocking
並不是所有blockingMethod
都支持中斷。例如input/outputStream
這兩個類,他們就不會拋出InterruptedException
,也不會因爲中斷而直接返回。在Socket I/O
而言,如果線程A關閉了Socket
,那麼正在socket
上讀寫數據的B、C、D都會拋出SocketException
. 非阻塞I/O(java.nio
)也不支持interruptiable I/O
,但是blocking operation
可以通過關閉channel
或者調用selector.wakeUp
方法來操作。類似的是,內部鎖(Synchronized Block
)也不能被中斷,但是ReentrantLock
是支持可被中斷模式的。
Non-Cancelable Tasks
有一些task設計出來就是不接受中斷請求,但是即便如此,這些task也需要restore中斷狀態,以便higher-level
的程序能夠在這個task執行完畢後響應中斷請求。
下面這段代碼就是一個BlockingQueue.poll()
忽略中斷的的例子(和上面我的備註一樣,不要在catch
裏面直接restore
狀態,不然queue.take()
會造成無限循環。
public Task getNextTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while (true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true;
// fall through and retry
}
}
} finally {
if (interrupted)
Thread.currentThread().interrupt();
}
}
Summary
你可以利用interruption mechanism來提供一個靈活的取消策略。任務可以自行決定他們是否可以取消,或者如何響應中斷請求,或者處理一些task relative cleanup。即便你想要忽略中斷請求,你也需要restore中斷狀態,當你catchInterruptedException的時候,當higher不認識他的時候,就不要拋出啦。
如果你的代碼在框架(包括JDK框架)中運行,那麼interruptException你就務必像上面一樣處理。如果單純的你自己的小代碼片段,那麼你可以簡單地認爲InterruptException就是個bug。
在生產環境下,tomcat shutdown的時候,也會大量關閉線程池,發出中斷請求。這個時候如果響應時間過於慢就會導致tomcat shutdown非常的慢(甚至於不響應)。所以大部分公司的重啓腳本中都含有重啓超時(例如20s)的一個強殺(kill -9
)的兜底策略,這個過程對於程序來說就等於物理上的斷電,凡是不可重試,沒有斷電保護,業務不冪等的情況都會產生大量的數據錯誤。
就現在業內的做法而言,大部分上述描述的問題幾乎已經不再通過interrupt這種關閉策略來解決(因爲實在難以解決),轉而通過整體的系統架構模型來規避數據問題,例如數據庫事務,例如可重試的冪等任務等等。
針對前端用戶而言,就是ng的上下線心跳切換。但即使如此,對於請求已經進入tomcat線程池中的前端用戶而言,還是會存在極其少量的服務器繁忙:)