在我進行 Java
程式設計實踐當中,特別是高效能程式設計時,執行緒池是無法逾越的高山。在最近攀登高山的路途上,我又雙叒叕掌握了一些優雅地使用執行緒池的技巧。
通常我會將非同步任務丟給執行緒池去處理,不怎麼會額外處理非同步任務執行中報錯。一般來說,任務執行報錯,會終止當前的執行緒,這樣執行緒池會建立新的執行緒執行下一個任務,當然是在需要建立執行緒和可以建立新執行緒的前提下。
在我最近一次實踐當中,發現一個定長 20 的執行緒池,已經建立過上萬個執行緒,這讓我大呼不可能。仔細一想,最終也在日誌當中確認了大量的非同步任務報錯。所以不得不讓我開始研究如何處理執行緒池中非同步任務的異常了。
以下是我的研究報告,誠邀各位共賞。
就我的水平而言,總計發現 5 種常見的異常處理方式。
try-catch
在提交非同步任務之前,通常我們會對非同步任務檢查異常進行處理,但是對於諸如 java.lang.RuntimeException
的非檢查異常不會做更多操作。
當我們提交非同步任務的時候,可以增加一個 try-catch
處理的話,就可以完全 hold 住非同步任務的可能丟擲的異常。
在我的框架設計當中,提交非同步任務有且僅一個入口,程式碼如下:
/
* 非同步執行某個程式碼塊
* Java呼叫需要return,Groovy也不需要,語法相容
*
* @param f
*/
public static void fun(Closure f) {
fun(f, null, true);
}
/
* 使用自定義同步器{@link FunPhaser}進行多執行緒同步
*
* @param f 程式碼塊
* @param phaser 同步器
*/
public static void fun(Closure f, FunPhaser phaser) {
fun(f, phaser, true);
}
/
* 使用自定義同步器{@link FunPhaser}進行多執行緒同步
*
* @param f
* @param phaser
* @param log
*/
public static void fun(Closure f, FunPhaser phaser, boolean log) {
if (phaser != null) phaser.register();
ThreadPoolUtil.executeSync(() -> {
try {
ThreadPoolUtil.executePriority(); //處理高優任務,可忽略
f.call();
} finally {
if (phaser != null) {
phaser.done();
if (log) logger.info("async task {}", phaser.queryTaskNum());
}
}
});
}
所以改造起來比較簡單,只需要在最後的方法中,增加 catch
程式碼塊即可。
/
* 使用自定義同步器{@link FunPhaser}進行多執行緒同步
* @param f
* @param phaser
* @param log
*/
public static void fun(Closure f, FunPhaser phaser, boolean log) {
if (phaser != null) phaser.register();
ThreadPoolUtil.executeSync(() -> {
try {
ThreadPoolUtil.executePriority();
f.call();
} catch (Exception e) {
logger.error("fun error", e);
} finally {
if (phaser != null) {
phaser.done();
if (log) logger.info("async task {}", phaser.queryTaskNum());
}
}
});
}
Callable
在 Java 中,Callable
是一種可以丟擲受檢異常(Checked Exception)的任務介面。這與 Runnable
的不同之處在於,Callable
能夠返回結果,並允許在任務執行過程中丟擲異常。異常處理通常在獲取任務結果時完成,以下是一些常見的處理方式。
Callable 異常處理的特點:
- 異常機制:
-
Callable
方法簽名為V call() throws Exception
,允許直接丟擲Exception
或其子類。 - 提交到執行緒池的
Callable
任務,如果丟擲異常,會被封裝到ExecutionException
中。
-
- 獲取異常:
- 透過
Future.get()
獲取結果時,若任務丟擲異常,則會引發ExecutionException
。 - 開發者需要在呼叫
get()
時捕獲和處理ExecutionException
。
- 透過
演示程式碼如下:
import java.util.concurrent.*;
public class CallableExceptionExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> task = () -> {
if (true) { // 模擬任務中出錯
throw new Exception("Simulated Exception");
}
return "Task Completed";
};
Future<String> future = executor.submit(task);
try {
// 獲取任務結果,可能丟擲 ExecutionException
String result = future.get();
System.out.println(result);
} catch (ExecutionException e) {
System.out.println("Caught an ExecutionException: " + e.getCause().getMessage());
} catch (InterruptedException e) {
System.out.println("Task was interrupted");
Thread.currentThread().interrupt(); // 恢復中斷狀態
}
}
}
控制檯輸出:
Caught an ExecutionException: Simulated Exception
當 Callable
丟擲異常時,執行緒池會捕獲這個異常,並將其封裝在 ExecutionException
中。如果任務在執行過程中被中斷,會丟擲 InterruptedException
。建議在捕獲時恢復執行緒的中斷狀態,以避免吞掉中斷訊號。
afterExecute()
在 Java 中,afterExecute()
是 ThreadPoolExecutor
提供的一個鉤子方法,允許開發者在每個任務執行完成後執行一些額外的邏輯。它可以用來捕獲執行緒池任務中丟擲的執行時異常和其他異常,從而進行集中處理或記錄。
afterExecute()
的作用:
- 觸發時機:
- 每當執行緒池中某個任務完成後,無論是正常完成還是丟擲異常,都會呼叫
afterExecute()
。 - 預設實現為空,使用者可以重寫它以新增自定義行為。
- 每當執行緒池中某個任務完成後,無論是正常完成還是丟擲異常,都會呼叫
- 異常捕獲:
- 如果任務在執行過程中丟擲異常(例如
RuntimeException
或Error
),它會作為引數傳遞給afterExecute()
的Throwable
引數。 - 需要手動從
Future
中獲取異常,或者在異常處理邏輯中記錄。
- 如果任務在執行過程中丟擲異常(例如
演示案例如下:
import java.util.concurrent.*;
public class AfterExecuteExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new CustomThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
// 提交一個會丟擲異常的任務
executor.submit(() -> {
System.out.println("Task started");
throw new RuntimeException("Task failed with exception");
});
executor.shutdown();
}
static class CustomThreadPoolExecutor extends ThreadPoolExecutor {
public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t); // 呼叫父類實現以確保正常行為
// 處理任務異常
if (t != null) {
System.err.println("Task threw an exception: " + t.getMessage());
} else if (r instanceof Future<?>) {
try {
((Future<?>) r).get(); // 獲取任務執行結果或捕獲異常
} catch (CancellationException e) {
System.err.println("Task was cancelled");
} catch (ExecutionException e) {
System.err.println("Task threw an exception: " + e.getCause().getMessage());
}
}
}
}
}
最終控制檯輸出:
Task started
Task threw an exception: Task failed with exception
如果需要在任務開始和結束時都執行邏輯,可以同時重寫 beforeExecute()
和 afterExecute()
。重寫此方法時,建議注意執行緒中斷訊號的恢復,並確保異常記錄邏輯不會引發額外的錯誤。
自定義 ThreadFactory
在 Java 中,如果需要自定義執行緒的異常處理行為,可以透過 自定義 ThreadFactory
建立執行緒並設定異常處理策略。執行緒的異常處理主要依賴於 Thread.UncaughtExceptionHandler
介面,該介面用於處理執行緒執行時未捕獲的異常。
步驟概覽:
- 建立自定義
ThreadFactory
:- 實現
ThreadFactory
介面,定製執行緒的建立邏輯。 - 在建立執行緒時,設定自定義的
UncaughtExceptionHandler
。
- 實現
- 實現異常處理邏輯:
- 使用
java.lang.Thread#setUncaughtExceptionHandler
,定義異常處理行為(如日誌記錄、傳送警報等)。
- 使用
- 將自定義
ThreadFactory
應用於執行緒池:- 在建立執行緒池時,透過
Executors
或ThreadPoolExecutor
使用自定義的ThreadFactory
。
- 在建立執行緒池時,透過
import java.util.concurrent.*;
public class CustomThreadFactoryExample {
public static void main(String[] args) {
// 使用自定義執行緒工廠建立執行緒池
ExecutorService executor = Executors.newFixedThreadPool(2, new CustomThreadFactory());
// 提交任務
executor.submit(() -> {
System.out.println("Task 1 started");
throw new RuntimeException("Task 1 encountered an error");
});
executor.submit(() -> System.out.println("Task 2 completed"));
executor.shutdown();
}
// 自定義 ThreadFactory 實現
static class CustomThreadFactory implements ThreadFactory {
private int threadId = 0;
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("CustomThread-" + threadId++);
thread.setUncaughtExceptionHandler(new CustomExceptionHandler());
return thread;
}
}
// 自定義異常處理器
static class CustomExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.err.println("Thread " + t.getName() + " threw an exception: " + e.getMessage());
// 這裡可以新增更多處理邏輯,例如日誌記錄或警報通知
}
}
}
控制檯輸出:
Task 1 started
Thread CustomThread-0 threw an exception: Task 1 encountered an error
Task 2 completed
使用 Executors.newFixedThreadPool()
並傳入自定義的 ThreadFactory
,讓執行緒池中的每個執行緒具備統一的異常處理行為。也可以透過執行緒標識為每個執行緒設定了自定義的 UncaughtExceptionHandler
。
全域性異常處理
在 Java 中,Thread.setDefaultUncaughtExceptionHandler
是一個全域性異常處理機制,用於處理所有未被捕獲的執行緒異常。與每個執行緒單獨設定的 Thread.setUncaughtExceptionHandler
不同,setDefaultUncaughtExceptionHandler
提供了一個全域性級別的異常處理器,適用於所有執行緒(除非執行緒單獨設定了自己的處理器)。
Thread.setDefaultUncaughtExceptionHandler
方法作用:
- 用於設定一個全域性的預設未捕獲異常處理器。
- 如果某個執行緒未顯式設定自己的
UncaughtExceptionHandler
,則會使用這個預設處理器。 - 通常用於記錄日誌、傳送報警等全域性異常處理邏輯。
演示程式碼如下:
public class DefaultExceptionHandlerExample {
public static void main(String[] args) {
// 設定全域性預設異常處理器
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
System.err.println("Unhandled exception in thread: " + thread.getName());
System.err.println("Exception: " + throwable.getMessage());
throwable.printStackTrace();
});
// 建立一個執行緒,丟擲未捕獲的異常
Thread thread1 = new Thread(() -> {
throw new RuntimeException("Thread 1 failed!");
});
thread1.start();
// 建立另一個執行緒,也丟擲未捕獲的異常
Thread thread2 = new Thread(() -> {
throw new RuntimeException("Thread 2 encountered an error!");
});
thread2.start();
}
}
控制檯如下:
Unhandled exception in thread: Thread-0
Exception: Thread 1 failed!
java.lang.RuntimeException: Thread 1 failed!
at ...
Unhandled exception in thread: Thread-1
Exception: Thread 2 encountered an error!
java.lang.RuntimeException: Thread 2 encountered an error!
at ...
全域性異常處理只針對主執行緒和未顯式設定 UncaughtExceptionHandler
的其他執行緒生效。
異常處理的優先順序:
- 如果執行緒顯式設定了自己的
UncaughtExceptionHandler
(透過thread.setUncaughtExceptionHandler
),那麼會優先呼叫該處理器。 - 如果執行緒未設定單獨的處理器,則呼叫全域性預設處理器。
- 如果沒有設定全域性預設處理器,未捕獲的異常將列印到標準錯誤輸出流。
如果主執行緒丟擲異常,Thread.setDefaultUncaughtExceptionHandler
無法捕獲它。需要在 main
方法中顯式處理。如果使用執行緒池(如 ExecutorService
),未捕獲的異常通常會被封裝為 ExecutionException
,不會觸發預設處理器。需要使用 Future.get()
或重寫 afterExecute()
來處理執行緒池的任務異常。
FunTester 原創精華
- 混沌工程、故障測試、Web 前端
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go
- 白盒、工具、爬蟲、UI 自動化
- 理論、感悟、影片