題記
在Java中沒有一種安全的搶佔方法來停止線程,因此也就沒有安全的搶佔式方法來停止任務。只有一些協作式的機制,使請求取消的任務和代碼都遵循一種協商好的協議。
響應中斷時執行的操作包括
- 清除中斷狀態
- 拋出InterrruptedException,表示阻塞操作由於中斷而提前結束
對中斷操作的正確理解
調用interrupt並不意味着立即停止目標線程正在進行的工作,而只是傳遞了請求中斷的消息。
它並不會真正地中斷一個正在運行的線程,而只是發出中斷請求,然後由線程在下一合適的時刻中斷自己。
論斷
- 通常,中斷是實現取消的最合理方式。
- 只有實現了線程中斷策略的代碼纔可以屏蔽中斷請求。在常規的任務和庫代碼中都不應該屏蔽中斷請求。
- 使用線程池往往比直接new Thread會更加方便,因爲線程池封裝了相關特性,如線程封裝,批量中斷機制等。
代碼響應方式
- 阻塞庫方法,捕捉InterruptedException,做好清除工作,然後結束線程
- 自己的代碼在合適的地方判斷isInterrupted(),以響應中斷
- 一些阻塞方法但不拋出InterruptedException的,需要具體討論,參見下述
處理不可中斷的阻塞
- Java.io包中的同步socket I/O。通過關閉底層的套接字,可以使得由於執行read或者write等方法而被阻塞的線程拋出一個SocketException。
- Java.io包中的同步I/O。當中斷一個正在InterruptibleChannel上等待的線程時,將拋出CloseByInterruptException並關閉鏈路
- Selector的異步I/O。如果一個線程在調用Selector.select方法(java.io.channels中)時阻塞了,那麼調用close或者wakeup方法會使線程拋出ClosedSelectorException並提前返回。
- 獲取一個鎖。如果一個線程由於等待某個內置鎖而阻塞,那麼將無法響應中斷。但在,在Lock類中提供了lockInterruptibly方法,該方法允許在等待一個鎖時仍能響應中斷。
區分任務與線程對中斷的反應是很重要的
你是中斷任務還是中斷線程,含義並不相同。比如在線程池裏,你只是中斷任務,處理完成異常,清除狀態之後,只需要簡單返回就行,但如果確定是要中斷線程,則要進行其它考慮了。
響應中斷
當調用可中斷的阻塞函數時,有兩種實用策略可用於處理InterruptedException
- 傳遞異常(可能在執行某個特定於任務的清除操作之後),從而使得你的方法也成爲可中斷的阻塞方法。
- 恢復中斷狀態,從而使得調用棧中的上層代碼能夠對其處理。
注意:只有實現了線程中斷策略的代碼纔可以屏蔽中斷請求。在常規的任務和庫代碼中都不應該屏蔽中斷請求。
線程池中的任務通過Future來實現取消,其中CANCEL API文檔說明
- 返回結果:返回FALSE:任務已經取消,已經完成等不能取消的原因。否則返回true
- 如果任務未開始執行,則不再執行
- 如果任務已經在運行了,則取決於參數mayInterruptIfRunning,true則會被中斷,false則不會中斷
線程池shutdownnow的侷限性:我們無法通過常規方法找出哪些任務已經開始但尚未結束
如果我們需要知道這些任務,並且不想直接中斷而是進行繼續的後續處理,則可以使用TrackingExecutor找出哪些任務已經開始但還沒有正常完成。在Executor結束後,getCancelledTasks返回被取消的任務清單。
public class TrackingExecutor extends AbstractExecutorService{
private final ExecutorService exec;
private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>());
....
public List<Runnable> getCancelledTasks(){
if(!exec.isTerminated()) throw new IllegalStateException(....);
return new ArrayList<Runnable>(tasksCancelledAtShutdown);
}
public void executor(final Runnable runnable){
//重點看這一段代碼
try{
runnable.run();
}finally{
if(isShutdown() && Thread.currentThread().isInterrupted())
tasksCancelledAtShutdown.add(runnable);
}
}
// 將ExecutorService其它方法委託給exec
}
//另一段使用TrackingExecutor類的代碼
public synchronized void stop() throws InterruptedException{
try{
saveUncrawled(exec.shutdownNow());
if(exec.awaitTermination(TIMEOUT,UNIT))
saveUncrawled(exec.getCancelledTasks);
}finally{
exec = null;
}
}
非正常的線程終止處理辦法
- 線程應該在try-catch塊中調用這些任務,這樣就能捕獲那些未檢查的異常了。或者也可以使用try-finally代碼塊來確保框架能夠知道線程非正常退出的情況,並做出正確的響應。你或者會捕獲RuntimeException異常,即當通過Runnable這樣的抽象機制來調用未知和不可信的代碼時。
- 在Thread API中同樣提供了UncaughtExceptionHandler,它能檢測出某個線程由於未捕獲的異常而終結的情況。
- 要爲線程池中的所有線程設置一個UncaughtExceptionHandler,需要爲ThreadPoolExecutor的構造函數提供一個ThreadFactory.
- 令人困惑的是,只有通過execute提交的任務,才能將它拋出的異常交給未捕捉異常處理器,而通過submit提交的任務,無論是拋出的未檢查異常還是已檢查異常,都將被認爲是任務返回狀態的一部分。如果一個由submit提交的任務由於拋出了異常而結束,那麼這個異常將被Future.get封裝在ExecutionException中重新拋出。
QUESTION:主線程退出,子線程沒有退出,JVM會退出嗎?
ANSWER:不會退出,因爲只要有非daemon線程未退出,JVM就不會退出。