取消與關閉(第七章)
取消與關閉
Java中沒有提供任何機製來安全得終止線程,但它提供了中斷(Interruption),這是一種協作機製,能夠使一個線程終止另一個線程當前的工作。
我們很少希望某個任務、線程或服務立即停止,因為這種立即停止會使共享的數據結構處於不一致的狀態。相反,在編寫任務和服務時可以使用一種協作的方式:當需要停止時,它們首先清楚當前正在執行的工作,然後再結束。
1.任務取消
一個可取消的任務必須擁有取消策略(Cancellation Policy),在這個策略中應詳細定義取消操作的“How”、“When”以及“What”,即其他代碼如何(How)請求取消任務,任務在何時(When)檢查是否已經請求了取消,以及在響應取消請求時應執行哪些(What)操作。
取消任務的方式:
1. 設置一個取消標誌,任務在每次迭代執行前首先檢查此標誌以確定任務是否被取消
2. 中斷。在任務中如果存在阻塞調用,那麼第一種方法將失效,此時,可以通過中斷方法取消任務
2.中斷
Thread中的中斷方法:
public class Thread{
public void interrupt(){...}
public boolean isInterrupted(){...}
/靜態的interrupted方法將清除當前線程的中斷狀態,並返回它之前的值,這也是清除中斷狀態的唯一方法/
public static boolean interrupted(){...}
}
調用interrupt並不意味著立即停止目標線程正在進行的工作,而隻是傳遞了請求中斷的消息。
對中斷操作的正確理解是:它並不會真正地中斷一個正在運行的線程,而隻是發出中斷請求,然後由線程在下一個合適的時刻中斷自己。
並非所有阻塞調用都能響應中斷操作,wait、sleep、join等將嚴格處理中斷請求,而IO阻塞並不能響應(常規的)中斷請求。
3.中斷策略
中斷策略規定線程如何解釋某個中斷請求----當發現中斷請求時,應該做哪些工作。
最合理的中斷策略是某種形式的線程級(Thread Level)取消操作或服務級(Service Level)取消操作:盡快推出,在必要時進行清理,同誌某個進程所有者該線程已推出。
大多數可阻塞的庫函數都隻是拋出InterruptedException作為中斷響應:盡快推出執行流程,並把中斷消息傳遞給調用者,從而使調用棧中的上層代碼可以采取進一步的操作。
當調用可中斷的阻塞方法,例如sleep和wait,有兩種策略可以用於處理InterruptedException:
1. 傳遞異常(可能在執行某個 特定於任務的清理後),從而使你的方法也能成為可中斷的阻塞方法
2. 恢複中斷狀態,從而使調用棧中的上層代碼能夠對其進行處理
通過Future來實現取消:
ExecutorService.submit將返回一個Future來描述任務。Future擁有一個cancel方法,該方法帶有一個boolean類型的參數mayInterruptIfRunning,表示操作能否接受中斷,如果為true並且任務正在某個線程中運行,那麼這個線程能被中斷,如果為false,那麼意味著“若任務還沒有啟動,就不要啟動它”。
當嚐試取消某個任務時,不宜直接中斷線程池,因為你並不知道當中斷請求到達時正在運行什麼任務----隻能通過任務的Future來實現取消。
4.處理不可中斷的阻塞
並非所有的可阻塞方法或阻塞機製都能響應中斷,如果一個線程由於執行同步的Socket I/O或者等待獲得內置鎖而阻塞,那麼中斷請求隻能設置線程的中斷狀態,除此之外沒有其他任何作用。對於那些由於執行不可中斷操作而被阻塞的線程,可以使用類似於中斷的手段來停止這些線程,但這要求我們必須知道線程阻塞的原因。
5.停止基於線程的服務
應用程序通常會創建擁有多個線程的服務,例如線程池,並且這些服務的生命周期通常比創建它們的方法的生命周期更長。如果應用程序準備退出,那麼這些服務所擁有的線程也需要結束。由於無法通過搶占式的方法來停止線程,因此它們需要自行結束。
正確的封裝原則是:除非擁有某個線程,否則不能對該線程進行操控,例如:中斷線程或者修改線程的優先級等。對於持有線程的服務,隻要服務的存在時間大於創建線程的方法的存在時間,那麼就應該提供生命周期方法。
線程的所有權是不可傳遞的:應用程序可以擁有服務,服務也可以擁有工作者線程,但應用程序並不能擁有工作者線程,因此應用程序不能直接停止工作者線程。
6.處理非正常的線程終止
導致線程提前死亡的最主要原因就是RuntimeException,由於這些異常表示出現了某種編程錯誤或者其他不可修複的錯誤,因此它們通常不會被捕獲,它們不會在調用棧中逐層傳遞,而是默認地在控製台中輸出棧追蹤信息,並終止線程。
除了主動捕獲異常,在Thread API中提供了UncaughtExceptionHandler,它能檢測出某個線程由於未捕獲的異常而終結的情況。當一個線程由於未捕獲異常而退出時,JVM會把這個事件報告給應用程序提供的UncaughtExceptionHandler,如果沒有提供任何異常處理器,那麼默認的行為是將棧追蹤信息輸出到System.err。
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
在運行時間較長的應用程序中,通常會為所有線程的未捕獲異常指定同一個異常處理器,並且該處理器至少會將異常信息記錄到日誌中。
7.JVM關閉
JVM既可以正常關閉,也可以強行關閉。正常關閉的觸發方式包括:
1. 當最後一個普通線程結束時(線程分為普通線程和守護線程)
2. 調用了System.exit
3. 通過其他特定於平台的方法關閉時(例如發送了SIGINT信號或鍵入Ctrl-C)
強行關閉JVM的方法:
調用Runtime.halt、在操作係統中kill JVM
1. 關閉鉤子
關閉鉤子(Shutdown Hook)是指通過Runtime.addShutdownHook注冊的但尚未開的線程。
在正常關閉中,JVM首先調用所有已注冊的關閉鉤子,但JVM並不能保證關閉鉤子的調用順序,在關閉應用程序線程時,如果有線程(包括守護線程)仍然在運行,那麼這些線程接下來將與關閉進程並發執行,當所有的關閉鉤子都執行結束時,如果runFinalizersOnExit為true,那麼JVM將運行終結器,然後再停止。如果關閉鉤子或終結器沒有執行完成,那麼正常關閉進程“掛起”並且JVM必須被強行關閉。當被強行關閉時,隻是關閉JVM,而不會運行關閉鉤子。
關閉鉤子不應該依賴那些可能被應用程序或其他關閉鉤子關閉的服務,實現這種功能的一種方式是對所有服務使用同一個關閉鉤子而不是每個服務使用一個不同的關閉鉤子。
一個關閉鉤子的例子:
public void start() {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
try {
LogService.this.stop();
catch (InterruptedException ignored) {}
}
}
});
}
最後更新:2017-11-04 18:03:50