從零開始學多執行緒之取消和關閉(六)
小節
為什麼需要取消和關閉: 有時候我們希望在任務或執行緒自然結束之前就停止它們,可能因為使用者取消了操作,或者應用程式需要快速關閉.
取消和關閉的好處: 不會浪費資源執行一些沒用的操作、保證程式的正常退出.
Java沒有提供任何機制,來安全地強迫執行緒停止手頭的工作.它提供中斷(執行緒的interrupt方法)--- 一個協作機制,使一個執行緒能夠要求另一個執行緒停止當前的工作.
立即停止執行緒的壞處:這種協作方法是必須的,因為我們很少需要一個任務、執行緒或者服務立即停止,立即停止會導致共享的資料結構處於不一致的狀態.任務和服務可以這樣編碼:當要求它們停止時,它們首先會清除當前程序中的工作,然後再終止.這提供了更好的靈活性,因為任務程式碼本身比發出取消請求的程式碼更明確應該清除什麼.
生命週期結束的問題使任務、服務以及程式的設計和實現變得複雜起來,這個程式設計中非常重要的元素卻經常被忽略.處理好失敗、關閉和取消是好的軟體和勉強執行的軟體最大的區別.(不銷燬執行執行緒,JVM無法退出)
任務取消
當外部程式碼能夠在活動自然完成之前,把它更改為完成狀態,那麼這個活動被稱為可取消的(cancellable).我們可能因為很多原因取消一個活動.
-
使用者請求的取消
-
限時活動
-
應用程式事件,一個任務發現瞭解決方案,所有其他仍在工作的搜尋會被取消
- 錯誤. 發生錯誤的時候,可能之前所有的任務都被取消
-
關閉. 一個優雅的關閉,可能允許當前的任務完成;在一個更加緊迫的關閉中,當前的任務就可能被取消了.
在Java中,沒有哪一種用來停止執行緒的方式是絕對安全的,因此沒有哪一種方式優先用來停止任務.這裡只有選擇相互協作的機制,通過協作,使任務和程式碼遵循一個統一的協議,用來請求取消.
在這些協作機制中,有一種會設定"cancellation requested",任務會定期檢視;如果發現標誌被設定過,任務就會提前結束.
public class cancellation { //退出迴圈的識別符號, volati保證可見性 private volatile boolean identify = true; public void cycle(){ while(identify){ System.out.println("持續輸出"); } } public void stop(){ identify = false; } }
執行執行緒在關閉程式的時候必須被取消,否則導致JVM不能正常退出.
一個可取消的任務必須擁有取消策略(cancellation policy),這個策略詳細說明關於取消的"how"、"when"、"what"---其它程式碼如何請求取消任務,任務在什麼時機檢查取消的請求是否到達,響應取消請求的任務中應有的行為.
中斷
上面的例子存在致命的缺陷:正常情況下,執行迴圈,列印輸出語句,可以正確的檢查識別符號狀態並退出迴圈,但是如果把輸出語句換成一個會阻塞的方法,例如BlockingQueue.put.那麼就存在方法被阻塞住了.導致永遠無法退出迴圈的可能.
public class Cancellation {
//退出迴圈的識別符號, volati保證可見性
private volatile boolean identify = true;
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
public void cycle(){
while(identify){
try {
//阻塞住了,無法執行到while判斷,永遠無法退出迴圈
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stop(){
identify = false;
}
}
好在queue.take()會響應中斷,當你能獲取到queue.take()的執行執行緒,並呼叫.interrupt()方法的時候,會結束阻塞並捕獲InterruptedException.
特定阻塞庫類的方法支援中斷,執行緒中斷是一個協作機制,一個執行緒給另一給執行緒傳送訊號(signal),通知它在方便或者可能的情況下停止正在做的工作,去做其他事情.
在API和語言規範中,並沒有把中斷與任何取消的語意繫結起來,但是實際上,使用中斷來處理取消之外的任何事情都是不明智的,並且很難支撐起更大的應用
每一個執行緒都有一個boolean型別的中斷狀態(interrupted status):在中斷的時候,這個中斷狀態被設定為true.
證明阻塞庫類可以響應執行緒中斷程式碼:
public class Cancellation {
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
public void cycle(){
try {
queue.take();
System.out.println("如果輸出這句話代表沒阻塞");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String [] args) throws InterruptedException {
Cancellation c = new Cancellation();
Thread t = new Thread(() -> c.cycle());
t.start();
Thread.sleep(1000);
t.interrupt();
//檢視中斷狀態
t.isInterrupted();
}
}
輸出:
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)
at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
at cn.bj.lbr.test.chap7.Cancellation.cycle(Cancellation.java:25)
at cn.bj.lbr.test.chap7.Cancellation.lambda$main$0(Cancellation.java:36)
at java.lang.Thread.run(Thread.java:748)
interrupt方法中斷目標執行緒,並且isInterrupted返回目標執行緒的狀態.
靜態的通過Thread.interrupted呼叫的方法僅能夠清除當前執行緒的中斷狀態,並返回他之前的值:這是清除中斷狀態唯一的方法.
阻塞庫函式例如Thread.sleep和Object.wait等,它們對中斷的響應表現為:清除中斷狀態,丟擲InterruptedException;這表示阻塞操作因為中斷的緣故提前結束.
當執行緒在並不處於阻塞狀態的情況下發生中斷時,會設定執行緒的中斷狀態,然後一直等到被取消的活動獲取中斷狀態,來檢查是否發生了中斷.如果不觸發InterruptedException,中斷狀態會一直保持,直到有人特意去清除中斷狀態.
呼叫interrupt並不意味著必然停止目標執行緒正在進行的工作;它僅僅傳遞了請求中斷的訊息.
我們對中斷本身最好的理解應該是:它並不會真正中斷一個正在執行的執行緒:它僅僅發出中斷請求,執行緒自己會在下一個方便的時候中斷(這些時刻被稱為取消點,cancellation point).有一些方法對這樣的請求很重視,比如wait、sleep和join方法,當它們接到中斷請求時會丟擲一個異常,或者進入時中斷狀態就已經被設定了.
Thread.interrupted(執行緒的靜態方法)應該小心使用,因為他會清除併發執行緒的中斷狀態.如果你呼叫了interrupted,並且它返回了true,你必須對其進行處理,除非你想掩蓋這個中斷-- 你可以丟擲InterruptedException,或者通過在次呼叫interrupt來儲存中斷狀態.
如果你的任務程式碼響應中斷,不要使用自定義的退出標識,使用執行緒的中斷作為你的取消機制,這樣在程式碼阻塞的時候你依然可以退出任務.示例:
public class Cancellation {
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
public void cycle(){
try {
//使用執行緒的中斷狀態作為取消的識別符號,保證在阻塞的時候依然可以取消任務
while(!Thread.currentThread().isInterrupted()) {
queue.take();
System.out.println("如果輸出這句話代表沒阻塞");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String [] args) throws InterruptedException {
Cancellation c = new Cancellation();
Thread t = new Thread(() -> c.cycle());
t.start();
Thread.sleep(1000);
t.interrupt();
boolean interrupted = Thread.interrupted();
}
}
上述程式碼呼叫了可中斷的take(),因此顯式的while(!Thread.currentThread.isInterrupted)不是絕對必要的,但是這種檢測會使程式碼對中斷具有更好的響應性.這是因為它在耗時的任務之前就檢查了中斷.而不是在任務完成之後才去檢查.當我們呼叫可中斷的阻塞方法時.通常並不能得到期望的響應,對中斷狀態進行顯示的檢測會對此有一定的幫助.
中斷通常是實現取消最明智的選擇
中斷策略
正如需要為任務制定取消策略一樣,也應該制定執行緒中斷策略(interruption policy).
一箇中斷策略決定執行緒如何應對中斷請求---當發現中斷請求時,它會做什麼(如果確實響應中斷的話),哪些工作單元對於中斷來說是原子的,以及在多快的時間裡響應中斷.
執行緒池實現以外的程式碼應該傳遞異常或者儲存中斷狀態:程式碼如果並不是執行緒的所有者(對於執行緒池而言,是指任何執行緒池實現以外的程式碼)就應該小心地儲存中斷狀態,這樣所有者的程式碼才能夠最終對其起到作用,甚至是"訪客"程式碼也能起到作用.
這就是為什麼大多數可阻塞的庫函式,僅僅丟擲InterruptedException作為中斷的響應.它們絕不可能執行在一個執行緒中,所以它們為任務或者庫程式碼實現了大多數合理的取消策略:它們會盡可能快地為異常資訊讓路,把它們向後傳給呼叫者,這樣上層棧的程式碼就可以進一步行動了.
當檢查到中斷請求時,任務並不需要放棄所有事情---它可以選擇推遲,直到更適合的時機.這需要記得它已經被請求過中斷了,完成當前正在進行的任務,然後丟擲InterruptedException或者指明中斷.當更新的過程中發生中斷時,這項技術能夠保證資料結構不被徹底破壞.
無論任務把中斷解釋為取消,還是其他的一些關於中斷的操作,它都應該注意儲存執行執行緒的中斷狀態.如果對中斷的處理不僅僅是把InterrutptedException傳遞給呼叫者,那麼它應該在捕獲InterruptedException之後恢復中斷狀態.
try {
//do somethings
} catch (InterruptedException e) {
e.printStackTrace();
//儲存中斷狀態
Thread.currentThread().interrupt();
}
執行緒應該只能被執行緒的所有者中斷:所有者可以把執行緒的中斷策略資訊封裝到一個合適的取消機制中,比如關閉(shutdown)方法中.
因為每一個執行緒都有其自己的中斷策略,所以你不應該中斷執行緒,除非你知道中斷這個執行緒意味著什麼.
響應中斷
當你呼叫可中斷的阻塞函式時,比如Thread.sleep或者BlockingQueue.put,有兩種處理InterruptedException的使用策略
- 傳遞異常(在方法名後面 throws InterrutedException,在呼叫這個方法的地方處理這個異常)
- 儲存中斷狀態,上層呼叫棧中的程式碼能夠對其進行處理
如果你不想,或不能傳遞InterruptedException(例如在Runnable中),你就應該在捕獲異常後呼叫Thread.currentThread.interrupt恢復中斷狀態.你不應該掩蓋InterruptedException,在catch快中捕獲到異常缺什麼也不做.
只有實現了執行緒中斷策略的程式碼才可以接收中斷請求,通用目的的任務和庫的程式碼絕不應該接受中斷請求.
在任務的外部執行緒中安排中斷:
public class InterruptTest {
//定任任務
private static final ScheduledExecutorService scheduledExecutorService= Executors.newScheduledThreadPool(1);
public static void timeRun(Runnable r, Long timeOut, TimeUnit timeUnit){
//得到當前執行緒
Thread t = Thread.currentThread();
//定時任務,超過引數設定的時間中斷當前執行緒.
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
t.interrupt();
}
},timeOut,timeUnit);
//執行任務
r.run();
}
}
示例程式碼是錯誤的,本意是r.run()超過限定的時長終止任務,但是如果r.run()已經執行完畢,定時任務啟動,就會導致終止了當前執行緒.但是此時執行緒執行什麼樣的程式碼是無法確定的,此時中斷肯定是錯誤的.
改正版:
public class InterruptTest {
//定任任務
private static final ScheduledExecutorService scheduledExecutorService= Executors.newScheduledThreadPool(1);
public static void timedRun(Runnable r,Long timeOut,TimeUnit timeUnit) throws InterruptedException {
//內部類,自定義一個run方法
class RethrowableTask implements Runnable{
private volatile Throwable t;
@Override
public void run() {
try {
//方法傳遞進來的r
r.run();
}catch (Throwable t){
this.t = t;
}
}
void rethrow() throws Throwable {
if(t != null){
throw t;
}
}
}
RethrowableTask task = new RethrowableTask();
//建立新的本地執行緒.用它來終止任務.
Thread t = new Thread(task);
t.start();
//超過時長中止執行緒
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
t.interrupt();
}
}, timeOut, timeUnit);
//t.join(),()內的時間引數就是讓t執行緒單獨執行的時間.在此期間內其他執行緒會阻塞
t.join(timeUnit.toMillis(timeOut));
try {
task.rethrow();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
改進的地方就是將執行任務的執行緒改為一個新的本地執行緒,這樣終止這個執行緒不會影響到其他任務.
通過Future取消
最終的結論: 取消任務就用Future.
Future可以管理任務的生命週期,處理異常,並有利於取消.
ExecutorService.Submit會返回一個Future來描述任務.Future有一個cancel方法,它需要一個boolean型別的引數,mayInterruptIfRunning,它的返回值表示取消嘗試是否成功(這僅僅告訴你它是否能夠接收中斷,而不是任務是否檢測並處理了中斷).當mayInterruptIfRunning為true,並且任務當前正在運行於一些執行緒中,那麼這個執行緒是應該中斷的.把這個引數設定成false意味著"如果還沒有啟動的話,不要執行這個任務",這應該用於那些不處理中斷的任務.
//假想的Future..
Future futuretask = ...
//設定true並且任務執行在一些執行緒中,那麼這個執行緒是應該中斷的
// 設定為false,如果沒啟動就不要運行了
boolean cancel = futuretask.cancel(true);
//返回值cancel僅僅告訴你能否中斷,而不是是否已經中斷了
除非你知道執行緒的中斷策略,否則你不應該中斷執行緒,那麼什麼時候可以採用一個true作為引數呼叫cancel?任務執行執行緒是由標準的Executor實現建立的,它實現了一箇中斷策略,使得任務可以通過中斷被取消,所以當它們在標準Executor中執行時,通過它們的Future來取消任務,這時設定mayInterruptIfRunning是安全的.
使用Future取消任務的好處(畫重點):當嘗試取消一個任務的時候,你不應該直接中斷執行緒池,因為你不知道中斷請求到達時,什麼任務正在執行---只能通過任務的Future來做這件事情.這便是編寫任務,讓它視中斷為取消請求的另一個理由:能夠通過它們的Future被取消.
使用future取消任務:
public class InterruptTest {
private static final ExecutorService executor = Executors.newFixedThreadPool(10);
public static void timedRun(Runnable r,Long timeOut,TimeUnit timeUnit){
Future f = executor.submit(r);
try {
f.get(timeOut,timeUnit);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
//executor裡面有中斷策略,所以這個 操作時執行緒安全的.
f.cancel(true);
}
}
}
當Future.get丟擲InterruptedException或TimeoutException時,如果你知道不再需要結果時,就可以呼叫Future.cancel來取消任務了.
處理不可中斷阻塞
支援中斷的可阻塞的庫方法提前返回和丟擲InterruptedException來實現對中斷的響應,但是有些不支援中斷的阻塞方法,例如同步Socket I/O或者等待獲得內部鎖而阻塞,那麼中斷除了能夠設定執行緒的中斷狀態該以外,什麼都不能改變.但是我們可以通過覆寫執行緒的interrupt或Future.cancel方法來自定義取消策略.
我們以同步的Socket I/O為例,在伺服器應用程式中,阻塞I/O最常見的形式是讀取和寫入Socket.InputStream和Output中的read和write方法都不響應中斷,但是通過關閉底層的Socket,可以讓read或write鎖阻塞的執行緒丟擲一個SocketException.
public class RenderThread extends Thread {
//final 安全釋出
private final Socket socket;
private final InputStream inputStream;
public RenderThread(Socket socket) throws IOException {
this.socket = socket;
this.inputStream = socket.getInputStream();
}
//重寫中斷方法
@Override
public void interrupt(){
try {
//關閉socket停止不可阻塞的方法
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run(){
//拿到byte
byte[] bytes = new byte['a'];
while (true){
try {
/*
* 從輸入流讀取資料的下一個位元組,如果這裡阻塞住了,
* 通過interrupt方法中斷
* */
int count = inputStream.read(bytes);
if(count< 0){
break;
}else{
//進行一些處理..
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
自定義Future取消不可中斷的阻塞操作
這個例子是上個例子的升級版,之前說過用Future中斷執行緒的方法是最簡單明瞭的,可以將Callable或者Runnable轉換成Future再取消,這是Java6中新增到ThreadPoolExecutor的新特性,當提交一個Callable給ExecutorService時,submit返回一個future,可以用Future來取消任務.執行緒池內部就是使用newTaskFor建立Future來代替任務.它返回一個RunnableFuture,這是一個介面,它擴充套件了Future和Runnable(並由FutureTask實現);
將Callable轉換成future的原始碼:
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
自定義的任務Future允許你覆寫Future.cancel方法.自定義的取消程式碼可以實現日誌或者收集取消的統計資訊,並可以用來取消那些不響應中斷的活動.
使用future更改上面的例子:
//類1
public interface CallableTask<T> extends Callable<T> {
//擴充套件的兩個方法
void cancel();
RunnableFuture<T> newTask();
}
//類2
public class CancellingExecutor extends ThreadPoolExecutor {
@Override
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable){
//判斷傳入的callable是不是我們自定義的callable的例項
if(callable instanceof CallableTask){
return ((CallableTask<T>) callable).newTask();
}else{
return super.newTaskFor(callable);
}
}
//類3
public class SocketUsingTask<T> implements CallableTask<T> {
private Socket socket;
protected synchronized void setSocket(Socket socket) {
this.socket = socket;
}
@Override
public synchronized void cancel() {
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public RunnableFuture<T> newTask() {
return new FutureTask<T>(this){
@Override
public boolean cancel(boolean mayInterruptIfRunning){
try {
SocketUsingTask.this.cancel();
} finally {
//返回值告訴你是否可以被中斷
return super.cancel(true);
}
}
};
}
}
解析:要通過future.cancel取消任務,當呼叫cancel方法的時候就要使socket關閉,這是類3實現的,使用類2的執行緒池可以返回我們自定義的future,而類1是個介面擴充套件類取消和封裝的方法,類3是他的實現類.
停止基於執行緒的服務
執行緒池擁有它的工作者執行緒,如果需要中斷這些執行緒,那麼應該由執行緒池負責.
ExecutorService提供了shutdown和shutdownNow方法,其他執行緒持有的服務也應該提供類似的關閉機制.
對於執行緒持有的服務,只要服務的存在時間大於建立執行緒的方法存在的時間,那麼就應該提供生命週期方法.
使用exec.shutdown()和exec.awaitTermination(TIMEOUT,UNIT)配合使用關閉執行緒服務.
awaitTermination方法:接收人timeout和TimeUnit兩個引數,用於設定超時時間及單位。當等待超過設定時間時,會監測ExecutorService是否已經關閉,若關閉則返回true,否則返回false。一般情況下會和shutdown方法組合使用。
shutdownNow的侷限性
當通過shutdownNow強行關閉一個ExecutorService時,它試圖取消正在進行的任務,並返回那些已經提交、但並沒有開始的任務的清單,這樣,這些任務可以被日誌記錄,或者存起來等待進一步處理.
但是那些已經開始、卻沒有結束的任務,卻沒有辦法被找出來.
儲存被取消的任務,示例:
public class TrackingExecutor extends AbstractExecutorService {
private final ExecutorService exec;
private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>());
public TrackingExecutor(ExecutorService exec) {
this.exec = exec;
}
public ArrayList<Runnable> getCancelledTasks(){
if(!exec.isTerminated()){
throw new IllegalStateException();
}
//返回一個新的list,不會發布這個set,否則還得加鎖,要不然有可見性問題
return new ArrayList<Runnable>(tasksCancelledAtShutdown);
}
@Override
public void execute(final Runnable command) {
exec.execute(new Runnable() {
@Override
public void run() {
try {
command.run();
} finally {
if(isShutdown() && Thread.currentThread().isInterrupted()){
//把任務放進取消任務的集合
tasksCancelledAtShutdown.add(command);
}
}
}
});
}
//省略若干方法
}
TrackingExecutor存在不可避免的競爭條件,使它產生假陽性(false positive)現象: 識別出的被取消任務事實上可能已經結束.產生的原因是在任務執行完之後,執行緒池記錄任務結束之前,執行緒池可能發生關閉.如果任務是冪等的(兩次執行得到的結果與一次相同),那麼這不會有什麼問題.應用程式得到已被取消的任務必須注意這個風險.
處理反常的執行緒終止
異常終止的執行緒會"死掉",執行緒池線上程死掉以後,可能會用新的執行緒取代這個工作執行緒,保證不能正常運轉的任務不會影響到後續任務的執行.
當執行緒失敗的時候,應用程式可能看起來仍在工作,所以它的失敗可能就會被忽略.幸運的是,我們有方法可以檢測和終止執行緒從程式中"洩漏".
導致執行緒死亡的最主要的原因是RuntimeException.因為這些異常表明一個程式錯誤或者其他不可修復的錯誤,它們通常是不能被捕獲的.它們不會順著棧的呼叫傳遞,此時,預設的行為實在控制檯列印追蹤的資訊,並終止執行緒.
任何程式碼都可以丟擲RuntimeException.無論何時,當你呼叫另一個方法,你都要對他的行為保持懷疑,不要盲目地認為它一定會正常返回,或者一定會丟擲在方法簽名中宣告的受檢查的異常.當你呼叫的程式碼越不熟悉,你就越應該堅持對程式碼行為的懷疑
如果任務丟擲了一個未檢查的異常,它將允許執行緒終結,但是會首先通知框架:執行緒已經終結.然後,框架可能會用心的執行緒取代這個工作執行緒,也可能不這樣做,因為執行緒池也許正在關閉,抑或當前已有足夠多多執行緒,能夠滿足需要了.
ThreadPoolExecutor使用這項技術確保那些不能正常運轉的任務不會影響到後續任務的執行,原始碼:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
為捕獲異常的處理
執行緒的API提供了UncaughtExceptionHandler的工具,使你能夠檢測到執行緒因未捕獲的異常引起的"死亡".這兩個方案互為補充:合在一起,組成了對抗執行緒洩漏點強有力的保障.
當一個執行緒因為未捕獲異常而退出時,JVM會把這個事件報告給應用程式提供的UncaughtExceptionHandler:如果處理器(handler)不存在,預設的行為是想Systeeem.err打印出站追蹤資訊.
實現UncaughtExceptionHandler介面
public class UEHlogger implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.SEVERE,"Thread terminated with exception:"+t.getName());
}
}
在一個長時間執行的應用程式中,所有的執行緒都要給未捕獲異常設定一個處理器,這個處理器至少要將異常資訊記入日誌中.
可以將剛剛自定義的UncaughtExceptionHandler作為引數(ThreadFachory)傳遞給執行緒池.標準執行緒池允許未 捕獲的任務異常去結束池執行緒,但是使用一個try-finally塊來接收通知的話,當池執行緒被終結後,能夠有新的執行緒取代它.如果沒有非捕獲異常的處理器,或者其他失敗通知機制,任務會無聲無息地失敗,這會導致混亂.如果你想在任務因為異常而失敗時獲得通知,那麼你應該採取一些特定任務的恢復行為,或者用Runnable與Callable把任務包裝起來,這樣就能夠捕獲異常,或者覆寫ThreadPoolExecutor的afterExecute鉤子(回撥)方法.
令人有些混淆的是,只有通過Execute提交的任務,才能將它丟擲的異常送交給未捕獲異常的處理器;而通過submit提交的任務,丟擲的任何異常,無論是否為受檢查的,都被認為是任務返回狀態的一部分.如果有submit提交的任務以異常作為終結,這個異常會被Future.get重丟擲,包裝在ExecutionException中.
關閉鉤子
關閉鉤子會在應用程式關閉的時候,執行一些自定義的操作(比如清理臨時檔案).
想使用關閉鉤子必須先要註冊鉤子,註冊:
public void hookTest(){
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run(){
System.out.println("退出時執行了關閉鉤子函式");
}
});
}
這樣在應用程式關閉的時候就會執行"退出時執行了關閉鉤子函式".
可以執行System.exit來退出應用程式:
public void exit(){
//引數0代表正常退出
System.exit(0);
}
注意必須先呼叫上面註冊關閉鉤子的方法註冊鉤子,才能在關閉的時候使用.
要使用鉤子函式必須保證關閉鉤子是執行緒安全的:它們在訪問共享資料時必須使用同步,並應該小心地避免死鎖.
使用關閉鉤子時對所有服務使用唯一的關閉鉤子,因為所有鉤子都是併發執行的不能保證順序,如果關閉一個服務依賴於另一個服務的時候這會引發問題.讓鉤子呼叫一些列關閉行為,而不是每個服務使用一個可以避免這個問題.
總結
任務、執行緒、服務以及應用程式在生命週期結束時的問題,可能會導致它們引入複雜的設計和實現.Java沒有提供具有明顯優勢的機制來取消活動或者終結執行緒.它提供了協作的中斷機制,能夠用來幫助取消,但是這將取決你如何構建取消的協議,並是否能一致地使用該協議.使用FutureTask和Executor框架可以簡化構建可取消的任務和服務.
來源:https://www.cnblogs.com/xisuo/p/9793202.html