Java 線程、線程池捕獲的異常,給我速速現身

前言

Java 開發的小伙伴們一定在開發、調試、單元測試過程中對于線程池的異常處理機制深感頭疼(我根本看不到異常,線程池直接捕獲了異常,并給該線程判了死刑)。在并發任務中,我們必然會引入線程池,可是在上線前誰也不能保證代碼的完美無缺,我們需要不斷測試,那么我們就必須讓線程池中捕獲的異常無所遁形。

普通線程的異常

既然說到普通線程,我們要考慮的就是我們放在線程執行任務的 run() 方法中的代碼。

public interface Runnable {
    public abstract void run();
}

那么如果 run() 方法中拋出了RuntimeException,將會怎么處理了?
通常java.lang.Thread對象運行設置一個默認的異常處理方法:

java.lang.Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)

而這個默認的靜態全局的異常捕獲方法時輸出堆棧。
當然,我們可以覆蓋此默認實現,只需要一個自定義的java.lang.Thread.UncaughtExceptionHandler接口實現即可。

public interface UncaughtExceptionHandler {
    void uncaughtException(Thread t, Throwable e);
}

線程池的異常

而在線程池中卻比較特殊。默認情況下,線程池 java.util.concurrent.ThreadPoolExecutor 會Catch住所有異常, 當任務執行完成(java.util.concurrent.ExecutorService.submit(Callable))獲取其結果 時(java.util.concurrent.Future.get())會拋出此RuntimeException。

/**
 * Waits if necessary for the computation to complete, and then
 * retrieves its result.
 *
 * @return the computed result
 * @throws CancellationException if the computation was cancelled
 * @throws ExecutionException if the computation threw an exception
 * @throws InterruptedException if the current thread was interrupted while waiting
 */
V get() throws InterruptedException, ExecutionException;

其中 ExecutionException 異常即是java.lang.Runnable 或者 java.util.concurrent.Callable 拋出的異常。
也就是說,線程池在執行任務時捕獲了所有異常,并將此異常加入結果中。這樣一來線程池中的所有線程都將無法捕獲到拋出的異常。 從而無法通過設置線程的默認捕獲方法攔截的錯誤異常。
也不同通過自定義線程來完成異常的攔截。
好在java.util.concurrent.ThreadPoolExecutor 預留了一個方法,運行在任務執行完畢進行擴展(當然也預留一個protected方法beforeExecute(Thread t, Runnable r)):

protected void afterExecute(Runnable r, Throwable t) { } 

此方法的默認實現為空,這樣我們就可以通過繼承或者覆蓋ThreadPoolExecutor 來達到自定義的錯誤處理。
解決辦法如下:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(11, 100, 1, TimeUnit.MINUTES, //
        new ArrayBlockingQueue<Runnable>(10000),//
        new DefaultThreadFactory()) {

    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        printException(r, t);
    }
};

private static void printException(Runnable r, Throwable t) {
    if (t == null && r instanceof Future<?>) {
        try {
            Future<?> future = (Future<?>) r;
            if (future.isDone())
                future.get();
        } catch (CancellationException ce) {
            t = ce;
        } catch (ExecutionException ee) {
            t = ee.getCause();
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt(); // ignore/reset
        }
    }
    if (t != null)
        log.error(t.getMessage(), t);
}

此辦法的關鍵在于,事實上 afterExecute 并不會總是拋出異常 Throwable t,通過查看源碼得知,異常是封裝在此時的Future對象中的, 而此Future對象其實是一個java.util.concurrent.FutureTask的實現,默認的run方法其實調用的 java.util.concurrent.FutureTask.Sync.innerRun()。

void innerRun() {
    if (!compareAndSetState(0, RUNNING))
        return;
    try {
        runner = Thread.currentThread();
        if (getState() == RUNNING) // recheck after setting thread
            innerSet(callable.call());
        else
            releaseShared(0); // cancel
    } catch (Throwable ex) {
        innerSetException(ex);
    }
}

void innerSetException(Throwable t) {
    for (;;) {
        int s = getState();
        if (s == RAN)
            return;
        if (s == CANCELLED) {
            // aggressively release to set runner to null,
            // in case we are racing with a cancel request
            // that will try to interrupt runner
            releaseShared(0);
            return;
        }
        if (compareAndSetState(s, RAN)) {
            exception = t;
            result = null;
            releaseShared(0);
            done();
            return;
        }
    }
}

這里我們可以看到它吃掉了異常,將異常存儲在java.util.concurrent.FutureTask.Sync的exception字段中:

/** The exception to throw from get() */
private Throwable exception;

當我們獲取異步執行的結果時, java.util.concurrent.FutureTask.get()

public V get() throws InterruptedException, ExecutionException {
    return sync.innerGet();
}

java.util.concurrent.FutureTask.Sync.innerGet()

V innerGet() throws InterruptedException, ExecutionException {
    acquireSharedInterruptibly(0);
    if (getState() == CANCELLED)
        throw new CancellationException();
    if (exception != null)
        throw new ExecutionException(exception);
    return result;
}

異常就會被包裝成ExecutionException異常拋出。
也就是說當我們想線程池 ThreadPoolExecutor(java.util.concurrent.ExecutorService)提交任務時, 如果不理會任務結果(Feture.get()),那么此異常將被線程池吃掉。

<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);

而java.util.concurrent.ScheduledThreadPoolExecutor是繼承ThreadPoolExecutor的,因此情況類似。

總結

通過覆蓋ThreadPoolExecutor.afterExecute 方法,我們才能捕獲到任務的異常(RuntimeException)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容