之前在使用自定義線程池異步執行耗時任務時,一直記着如果業務方法拋出異常沒有捕獲,那么是看不到日志框架輸出的異常日志的,所以總是在業務方法中包裹一層try-catch捕獲可能發生的異常。也未去深入為什么沒有打印異常日志和解決方法,這里記錄下自己的總結。
1、事例
@Slf4j @SpringBootTest @RunWith(SpringRunner.class) public class ThreadPoolExecutorTest { //這里直接使用ThreadPoolExecutor類,這里沒有自定義ThreadFactory,默認使用的是Executors.defaultThreadFactory() private ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(10)); @Test public void testSubmitTask() throws InterruptedException { log.info("開始提交任務"); threadPool.execute(this::doSomeThing); log.info("提交任務完成"); TimeUnit.SECONDS.sleep(10); } private void doSomeThing() { int value = 10 / 0; } }
我們在IDEA運行時,控制台是有打印出錯誤日志的(注意此時並沒有設置日志輸出到控制台的Appender),實際線上環境我們一般也只配置日志框架輸出到日志文件的,那么這個日志是誰打印的呢?這里先不去深追,后面詳解。
控制台輸出日志:
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero at org.layman.study.javainaction.ThreadPoolExecutorTest.doSomeThing(ThreadPoolExecutorTest.java:33) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
上面的日志輸出結果去我們配置的日志文件查找是找不到的,那么我之前的做法是在doSomeThing方法中使用try-catch包裹:
private void doSomeThing() { try { int value = 10 / 0; } catch (RuntimeException e) { log.error("執行異常:", e); } }
這次執行結果如下,看日志輸出我們能明確知道 logger 為org.layman.study.javainaction.ThreadPoolExecutorTest類(注意第3行日志,可以對比下上面的日志輸出結果):
2019-09-05 11:04:32.161 logback [main] INFO o.l.s.j.ThreadPoolExecutorTest - 開始提交任務 2019-09-05 11:04:32.162 logback [main] INFO o.l.s.j.ThreadPoolExecutorTest - 提交任務完成 2019-09-05 11:04:32.164 logback [pool-1-thread-1] ERROR o.l.s.j.ThreadPoolExecutorTest - 執行異常: java.lang.ArithmeticException: / by zero at org.layman.study.javainaction.ThreadPoolExecutorTest.doSomeThing(ThreadPoolExecutorTest.java:40) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
2、溯源
通常我們提交給線程池的任務后,真正幫我們執行的是Thread.Worker這個類,該類是Thread的內部類,且實現了Runnable接口,直接看run方法實現:
如果不熟悉線程池內部原理,可以去Google下
//Thread$Worker.java public void run() { runWorker(this); } //Thread.java final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; //省略代碼。。。 try { while (task != null || (task = getTask()) != null) { //省略代碼。。。 try { beforeExecute(wt, task); //㈠任務的前置處理 Throwable thrown = null; //重要關注點 try { //㈡ 調用我們業務方法 task.run(); } catch (RuntimeException x) { //㈢ 捕獲RuntimeException 類型異常 thrown = x; throw x; } catch (Error x) { //㈣ 捕獲RuntimeException 類型異常 thrown = x; throw x; } catch (Throwable x) {//㈤ 捕獲Throwable 類型異常,重新拋出Error異常 thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); // ㈥ 任務的后置處理 } } finally { //省略代碼。。。 } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }
㈠:執行任務的前置處理,空實現。
㈡ :調用執行我們提交的任務,但是 Runnable task = w.firstTask獲取的task可能是包裝我們提交task的包裝類。
㈢、㈣、㈤:從這里可以發現線程池內部捕獲並記錄異常后重新拋出,在重新拋出捕獲的異常前,會把捕獲的異常交由afterExecute(task, thrown)處理。
㈥:執行任務的后置處理,空實現。
在注釋㈡處就是執行我們的業務方法,按理我們業務方法拋出異常后,這里也會重新拋出,那么我們查看日志時為什么沒有看到打印出錯誤日志呢?
在分析具體原因前,在補充下我們向線程池提交任務的兩種方式
- public void execute(Runnable command),及其重載方法
- public <T> Future<T> submit(Callable<T> task),及其重載方法
我們先從簡單的來,先從submit(Callable<T> task) 下手。
2.1、java.util.concurrent.AbstractExecutorService#submit(java.util.concurrent.Callable<T>)
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); }
㈠:調用newTaskFor()方法將我們提交的task包裝為了FutureTask。
㈡:調用execute方法提交任務,最終線程池會調用FutureTask的run方法。
FutureTask實現了Runnable接口,所以這里能夠傳遞給execute()方法。
2.1.1 java.util.concurrent.FutureTask
run方法實現:
//FutureTask.java public void run() { // 省略部分代碼。。。。 try { Callable<V> c = callable; //我們提交task的引用 if (c != null && state == NEW) { V result; boolean ran; try { result = c.call(); //㈠ 執行我們提交的task ran = true; } catch (Throwable ex) { // ㈡重點關注處 result = null; ran = false; setException(ex); // ㈢ } if (ran) set(result); } } finally { //省略部分代碼。。。 } }
㈠:調用執行我們實際提交的task
㈡:當我們提交的task 拋出異常時,這里進行了捕獲並且沒有重新拋出。
㈢:調用setException(ex)方法保存異常和設置執行狀態,詳情見2.1.2 java.util.concurrent.FutureTask#setException。
2.1.2、java.util.concurrent.FutureTask#setException(ex)
//FutureTask.java /** The result to return or exception to throw from get() */ //保存要返回的結果或異常,在調用get()時如果有異常(通過狀態判斷)則拋出異常,否則返回執行結果。 private Object outcome; // non-volatile, protected by state reads/writes protected void setException(Throwable t) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = t; //㈠ 將異常信息設置給outcome對象 UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state ㈡ 設置最終狀態為EXCEPTIONAL finishCompletion(); } }
㈠:處使用outcome保存拋出的異常信息
㈡:處 將FutureTask的執行狀態設置為EXCEPTIONAL
2.1.3、java.util.concurrent.FutureTask#get()
//FutureTask.java private static final int COMPLETING = 1; private static final int NORMAL = 2; private static final int EXCEPTIONAL = 3; private static final int CANCELLED = 4; /** * @throws CancellationException {@inheritDoc} */ public V get() throws InterruptedException, ExecutionException { int s = state; //㈠ state = EXCEPTIONAL狀態 if (s <= COMPLETING) s = awaitDone(false, 0L); return report(s); } private V report(int s) throws ExecutionException { Object x = outcome; if (s == NORMAL) return (V)x; if (s >= CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); //㈡ 最終執行到這里拋出異常 }
㈠:在setException方法中已經將狀態設置為了EXCEPTIONAL,所以這里 s = EXCEPTIONAL,后續調用report方法。
㈡:在report方法中s 都不滿足兩個if條件,最后會將業務拋出的異常包裝為ExecutionException 后重新拋出。
2.1.4 小結
- 通過submit方法提交任務時,線程池內部會將我們提交的task包裝為FutureTask。
- FutureTask的run方法捕獲了我們業務方法拋出的異常,在調用java.util.concurrent.FutureTask#get()方法時拋出異常。
2.2 java.util.concurrent.ThreadPoolExecutor#execute
在“事例”中就是使用的該種方式提交task,從結果來看,當業務方法拋出異常后控制台打印日志而日志文件中卻沒有錯誤日志,那么這里我們來看下究竟是誰打印的異常日志。
這里我首先懷疑是Thread類打印的,去翻看Thread類定義時找到一些蛛絲馬跡,Thread類中有一個 setUncaughtExceptionHandler(UncaughtExceptionHandler eh)方法。
2.2.1 java.lang.Thread#setUncaughtExceptionHandler
/** * Set the handler invoked when this thread abruptly terminates due to an uncaught exception.// ㈠ * <p>A thread can take full control of how it responds to uncaught * exceptions by having its uncaught exception handler explicitly set. * If no such handler is set then the thread's <tt>ThreadGroup</tt> * object acts as its handler. * @param eh the object to use as this thread's uncaught exception * handler. If <tt>null</tt> then this thread has no explicit handler. * @throws SecurityException if the current thread is not allowed to * modify this thread. * @see #setDefaultUncaughtExceptionHandler * @see ThreadGroup#uncaughtException * @since 1.5 */ public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) { checkAccess(); uncaughtExceptionHandler = eh; }
㈠:“設置當此線程由於未捕獲異常突然終止時調用的處理程序”。方法注釋已經明確告知我們方法的用途了,並且還告訴我們,如果我們沒有設置該handler那么ThreadGroup將充當handler進行處理,那么我們在看下在哪里調用了getter方法。
其實Thread類中還有另一個設置處理器的方法,java.lang.Thread#setDefaultUncaughtExceptionHandler,這是一個靜態方法,設置的handler是一個全局處理器,及當我們沒有設置handler時,回去找這個默認的handler,如果還是沒有的話ThreadGroup會進行處理。
2.2.2 java.lang.Thread#dispatchUncaughtException
/** * Dispatch an uncaught exception to the handler. * This method is intended to be called only by the JVM. */ private void dispatchUncaughtException(Throwable e) { getUncaughtExceptionHandler().uncaughtException(this, e); } /** * Returns the handler invoked when this thread abruptly terminates * due to an uncaught exception. If this thread has not had an * uncaught exception handler explicitly set then this thread's * <tt>ThreadGroup</tt> object is returned, unless this thread * has terminated, in which case <tt>null</tt> is returned. * @since 1.5 * @return the uncaught exception handler for this thread */ public UncaughtExceptionHandler getUncaughtExceptionHandler() { return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group; }
最終在dispatchUncaughtException方法中找到調用了getUncaughtExceptionHandler方法,但是想再繼續找Thread 內部誰調用了dispatchUncaughtException時,卻沒有找到任何調用的地方,並且該方法為private 修飾那么外部是不可能調用的。
當看到方法注釋后如醍醐灌頂,"調用處理器處理異常,該方法只有JVM調用",划重點了。(JVM老大,你說啥就是啥,你想怎么調都行)
默認情況下我們是沒有設置handler的,所以這里會返回ThreadGroup的實例,所以我們直接來看ThreadGroup類中uncaughtException的實現
2.2.3 java.lang.ThreadGroup#uncaughtException
/** * Called by the Java Virtual Machine when a thread in this * thread group stops because of an uncaught exception, and the thread * does not have a specific {@link Thread.UncaughtExceptionHandler} * installed. * <p> * The <code>uncaughtException</code> method of * <code>ThreadGroup</code> does the following: * <ul> * <li>If this thread group has a parent thread group, the * <code>uncaughtException</code> method of that parent is called * with the same two arguments. * <li>Otherwise, this method checks to see if there is a * {@linkplain Thread#getDefaultUncaughtExceptionHandler default * uncaught exception handler} installed, and if so, its * <code>uncaughtException</code> method is called with the same * two arguments. * <li>Otherwise, this method determines if the <code>Throwable</code> * argument is an instance of {@link ThreadDeath}. If so, nothing * special is done. Otherwise, a message containing the * thread's name, as returned from the thread's {@link * Thread#getName getName} method, and a stack backtrace, * using the <code>Throwable</code>'s {@link * Throwable#printStackTrace printStackTrace} method, is * printed to the {@linkplain System#err standard error stream}. * </ul> * <p> * Applications can override this method in subclasses of * <code>ThreadGroup</code> to provide alternative handling of * uncaught exceptions. * * @param t the thread that is about to exit. * @param e the uncaught exception. * @since JDK1.0 */ public void uncaughtException(Thread t, Throwable e) { if (parent != null) { parent.uncaughtException(t, e); } else { Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler(); if (ueh != null) { ueh.uncaughtException(t, e); } else if (!(e instanceof ThreadDeath)) { // 該日志是不會記錄到日志文件中的,如果環境是tomcat的話最終會打到catalina.out,在本地的話標准異常輸出就對應着我們的控制台,正如事例中我們在控制台看到了日志輸出。 System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err); } } }
方法注釋很長,但是傳遞給我們的信息確是非常重要的,這里闡述了該方法的執行機制:
- 當線程因為未捕獲異常終止切沒有設置異常處理器時,會調用該方法。
- 如果ThreadGroup有parent,則調用parent.uncaughtException()。
- 如果ThreadGroup沒有parent時
>調用Thread.getDefaultUncaughtExceptionHandler()獲取全局處理器,如果獲取到則進行處理
>如果沒有獲取到全局處理器且異常不是ThreadDeath類型,通過System.err.print()打印日志,且打印了堆棧信息,該輸出日志不會打印到日志文件中,如果環境是tomcat的話最終會打到catalina.out。這里就和我們“事例”程序中的輸出對應上了,是不是非(hui)常開心
方法上也說明,我們可以自定義類繼承ThreadGroup來覆寫uncaughtException 方法提供額外的處理。
3、異常處理
通過以上代碼的跟蹤與梳理,我們至少可以采用如四種方式進行異常處理
3.1、最簡單的方式
直接在我們的業務方法中直接try-catch捕獲所有的異常,直接在catch塊中進行異常處理。
3.2、自定義ThreadPoolExecutor類
public class CustomThreadPoolExecutor extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); //自定義異常的處理邏輯 } }
java.util.concurrent.ThreadPoolExecutor.Worker 是線程池內部正真干事兒的,最終會調用java.util.concurrent.ThreadPoolExecutor#runWorker,該方法在調用執行業務方法前后都有回調方法,在finally塊中會回調afterExecute(Runnable r, Throwable t),所以我們可以利用這個回調方法處理可能拋出的異常。
3.3、自定義ThreadGroup類
public class CustomThreadGroup extends ThreadGroup { @Override public void uncaughtException(Thread t, Throwable e) { super.uncaughtException(t, e); //自定義異常的處理邏輯 } } //創建線程池時自定義創建線程,並給線程設置自定義的CustomThreadGroup ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(10), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(new CustomThreadGroup("CustomThreadGroup"), r); } });
在我們沒有設置UncaughtExceptionHandler時,最終會有ThreadGroup來處理未捕獲的異常,所以我們可以自定義ThreadGroup類覆寫uncaughtException方法添加額外的異常處理。
3.4、設置UncaughtExceptionHandler
public class CustomUncaughtExceptionHandler implements UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { //自定義異常處理邏輯 } } //創建線程池時自定義創建線程,並給線程設置自定義的UncaughtExceptionHandler ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(10), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setUncaughtExceptionHandler(new CustomUncaughtExceptionHandler()); return thread; } });
這種方式設置異常處理器更加靈活,我更傾向於使用該種方式,另外我們也可以調用java.lang.Thread#setDefaultUncaughtExceptionHandler 設置一個全局的默認異常處理器,但是通過給每個線程設置異常處理器更加便於我們管理,這個我們可以酌情使用。
4、總結
- java線程池會捕獲任務拋出的異常和錯誤,處理策略會受到我們提交任務的方式而不同。
- submit()方式提交的任務會返給我們一個Future,如果業務方法有拋出異常,當我們調用java.util.concurrent.Future#get()方法時會拋出包裝后的java.util.concurrent.ExecutionException。
- execute()方式提交的任務,java處理的默認策略是使用System.err.print("Exception in thread \"" + t.getName() + "\" ")輸出日志,但是該日志不會打印到我們的日志文件中,如果線上環境是tomcat的話最終會打到catalina.out文件中。
- 可以修改java線程池的默認處理策略,具體修改方式見上面 “第3點、異常處理”
至此,對java線程池內部針對異常處理有了一個深入的了解。