執行緒池異常處理的 5 中方式

FunTester發表於2024-12-18

在我進行 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 異常處理的特點:

  1. 異常機制:
    • Callable 方法簽名為 V call() throws Exception,允許直接丟擲 Exception 或其子類。
    • 提交到執行緒池的 Callable 任務,如果丟擲異常,會被封裝到 ExecutionException 中。
  2. 獲取異常:
    • 透過 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() 的作用:

  1. 觸發時機:
    • 每當執行緒池中某個任務完成後,無論是正常完成還是丟擲異常,都會呼叫 afterExecute()
    • 預設實現為空,使用者可以重寫它以新增自定義行為。
  2. 異常捕獲:
    • 如果任務在執行過程中丟擲異常(例如 RuntimeExceptionError),它會作為引數傳遞給 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 介面,該介面用於處理執行緒執行時未捕獲的異常。

步驟概覽:

  1. 建立自定義 ThreadFactory
    • 實現 ThreadFactory 介面,定製執行緒的建立邏輯。
    • 在建立執行緒時,設定自定義的 UncaughtExceptionHandler
  2. 實現異常處理邏輯:
    • 使用 java.lang.Thread#setUncaughtExceptionHandler,定義異常處理行為(如日誌記錄、傳送警報等)。
  3. 將自定義 ThreadFactory 應用於執行緒池:
    • 在建立執行緒池時,透過 ExecutorsThreadPoolExecutor 使用自定義的 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 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章