序: 任務和線程的啓動很容易。在大多數時候,我們會讓他們運行直到結束,或者讓他們自行停止。然而有時候我們希望提前結束任務或者線程,獲取因爲用戶取消了操作,或者用戶程序需要被快速關閉。
1、任務取消
1.1、中斷
public class Thread {
//能中斷目標線程
public void interrupt() {...}
//返回中斷線程的目標狀
public boolean isInterrupted() {...}
//清除當前線程的中斷狀態,並返回它之前的值,這也是清除中斷狀態的唯一方法
public static boolean interupted() {...}
}
理解:它並不會真正中斷一個正在運行的線程,而只是發出中斷請求,然後由線程在下一個合適的時刻中斷自己。(這些時刻也被稱爲取消點)
有些方法,例如wait、sleep和join等,將嚴格處理這些請求,當他們收到中斷請求或者在開始執行時發現某個已被設置好的中斷狀態時,將拋出一個異常。(InterruptedException)
在JAVA的API或者語言規範中,並沒有將中斷與任何取消語義關聯起來,但實際上在取消之外的其它操作中使用中斷的話都是不合適的,並且很難支撐起更大的系統。通常,中斷是實現取消的最合理方式。
1.2、中斷策略
當發現中斷請求時,應該做哪些操作。
線程應該只能由其所有者中斷,所有者可以將線程的中斷策略信息封裝到某個合適的取消機制中,例如shutdown方法。每個線程擁有各自的中斷策略,除非你知道中斷對此線程意味着什麼,否則就不應該中斷該線程。
如果除了將InterruptedException傳遞給調用者外還需要執行其它操作,那麼應該在捕獲InterruptedException後恢復中斷狀態:Thread.currentThread().interrupt();
1.3、通過Future來實現取消
當Future.get()拋出IinterruptedException或者TimeoutException時,如果你知道不再需要結果,那麼就可以調用Future.cancel來取消任務。
1.4、採用newTaskFor來封裝非標準的取消
Java6在ThreadPoolExecutor中新增的功能。當把一個Callable提交給ExeceutorService時,submit方法會返回一個Future,我們可用通過這個Future來取消任務。newTaskFor是一個工廠方法,它將創建Future來代表任務。newTaskFor還能返回一個RunableFuture接口,該接口擴展了Future和Runable(並由FutureTask實現)。通過定製表示任務的Future可以改變Future.cancel的行爲。
2、停止基於線程的服務
應用程序通常會創建擁有多個線程的服務,如線程池,並且這些服務端的生命週期通常比創建他們的方法的生命週期更長。如果應用程序準備退出,如果應用程序退出,那麼這些服務所擁有的線程也需要結束。
正確的封裝原則時:除非擁有某個線程,否則不能對該線程進行操控。例如中斷線程或者修改線程的優先級等。線程有一個相應的所有者,即創建該線程的類。因此線程池是其工作線程的所有者,如果要中斷這些線程,那麼應該使用線程池。
與其它封裝對象一樣,線程的所有權是不可傳遞的。應用程序擁有服務,服務擁有工作者線程,但應用程序不能擁有工作者線程,因此應用程序不能直接停止線程。相反服務應該提供生命週期來關閉它自己以及它擁有的線程。
2.1、關閉ExecutorService
使用shutdown正常關閉,速度慢,更安全,因爲ExecutorService會一直等到隊列中的所有任務都執行完後才關閉。
使用shutdownNow強行關閉,首先關閉當前正在執行的任務,然後返回所有尚未執行的任務清單。強行關閉速度塊,風險高,因爲任務有可能在執行一半時就被關閉了。然而TrackingExecutor可以找出哪些任務已經開始但還沒有正常完成。在Exectutor結束後,getCancekedTasks返回被取消的任務清單,用使這項技術發揮作用,任務在返回時必須維持線程的中斷狀態。
2.2、“毒丸”對象
“毒丸”是指一個放在隊列上的對象,當得到這個對象時,立即停止。只有在生產者和消費者數量都已知的情況下才能使用毒丸對象。
3、處理非正常的線程終止
某個線程發生了未捕獲的異常而終止。可以考慮使用try-catch代碼塊來調用這些任務,或者也可以使用try-finally代碼塊調用這些任務。
當一個線程由於未捕獲異常而退出時,JVM會把這個事件報告給應用程序提供的UncaughtExceptionHandler,它能檢測出某個線程由於未捕獲的異常而終結的情況。
public interface UncaughtExceptionHandler {
void uncaughtException (Thread t, Throwable e);
}
只有通過execute提交的任務,才能將它拋出的異常交給未捕獲異常處理器,而通過submit提交的任務,無論拋出未檢查異常還是已檢查異常,都將被認爲是任務返回狀態的一部分。如果一個由submit提交的任務拋出了異常而結束,那麼這個異常將被Future.get封裝在ExecutionException中重新拋出。
4、JVM關閉
JVM既可以正常關閉,也可以強行關閉。
4.1、關閉鉤子
在正常關閉中,JVM首先調用所有已註冊的關閉鉤子。關閉鉤子是指通過Runtime.addShutdownHook註冊的但尚未開始的線程。當所有的關閉鉤子都執行結束時,如果runFinalizersOnExit爲true,那麼JVM將運行終接器,然後再停止。關閉鉤子應該是線程安全的。
//通過註冊一個關閉鉤子來停止日誌服務
public void start() {
Runtime.getRuntime().addShutdownHook(
new Thread() {
public void run() {
try{
LogService.this.stop();
} catch(InterruptedException ignored) {
}
}
}
);
}
4.2、守護線程
線程分爲普通線程和守護線程,兩者之間差異在於線程退出時發生的操作。當一個線程退出時,JVM會檢查其它正在運行的線程,如果這些線程都是守護線程,那麼JVM會正常退出操作。當JVM停止時,所有的守護線程都會被拋棄(即不會執行finally塊,也不會執行回捲棧,而JVM只是直接退出)。
4.3、終結器
避免使用終結器。
參考《Java併發編程實戰》