今天遇到了一個bug,現象是,一個任務放入線程池中,彷佛「沒有被執行」,日誌也沒有打。html
通過本地代碼調試以後,發如今任務邏輯的前半段,拋出了NPE
,可是代碼外層沒有try-catch
,致使這個異常被吃掉。java
這個問題解決起來是很簡單的,外層加個try-catch
就行了,可是這個異常若是沒有被catch,線程池內部邏輯是怎麼處理這個異常的呢?這個異常最後會跑到哪裏呢?多線程
帶着疑問和好奇心,我研究了一下線程池那一塊的源碼,而且作了如下的總結。ide
項目中出問題的代碼差很少就是下面這個樣子oop
ExecutorService threadPool = Executors.newFixedThreadPool(3); threadPool.submit(() -> { String pennyStr = null; Double penny = Double.valueOf(pennyStr); ... })
先進到newFixedThreadPool
這個工廠方法中看生成的具體實現類,發現是ThreadPoolExecutor
源碼分析
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
再看這個類的繼承關係,this
再進到submit
方法,這個方法在ExecutorService
接口中約定,實際上是在AbstractExectorService
中實現,ThreadPoolExecutor
並無override這個方法。spa
public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; } protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { return new FutureTask<T>(runnable, value); }
對應的FutureTask對象的
構造方法.net
public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // state由volatile 修飾 保證多線程下的可見性 }
對應Callable
對象的構造方法線程
public static <T> Callable<T> callable(Runnable task, T result) { if (task == null) throw new NullPointerException(); return new RunnableAdapter<T>(task, result); }
對應RunnableAdapter
對象的構造方法
/** * A callable that runs given task and returns given result * 一個能執行所給任務而且返回結果的Callable對象 */ static final class RunnableAdapter<T> implements Callable<T> { final Runnable task; final T result; RunnableAdapter(Runnable task, T result) { this.task = task; this.result = result; } public T call() { task.run(); return result; } }
總結上面的,newTaskFor
就是把咱們提交的Runnable
對象包裝成了一個Future
。
接下來就是會把任務提交到隊列中給線程池調度處理:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }
由於主要關心的是這個線程怎麼執行,異常的拋出和處理,因此咱們暫時不解析多餘的邏輯。很容易發現,若是任務要被執行,確定是進到了addWorker
方法當中,因此咱們再進去看,鑑於addWorker
方法的很長,不想列太多的代碼,我就摘了關鍵代碼段:
private boolean addWorker(Runnable firstTask, boolean core) { ... boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { // 實例化一個worker對象 w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { int rs = runStateOf(ctl.get()); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { // 從Worker對象的構造方法看,當這個thread對象start以後, // 以後實際上就是調用Worker對象的run() t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; } // Worker的構造方法 Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); }
咱們再看這個ThreadPoolExecutor
的內部類Worker
對象:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable { ... /** Delegates main run loop to outer runWorker */ public void run() { runWorker(this); } ... }
看來真正執行任務的是在這個外部的runWorker
當中,讓咱們再看看這個方法是怎麼消費Worker
線程的。
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 ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; // ==== 關鍵代碼 start ==== try { // 很簡潔明瞭,調用了任務的run方法 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); } // ==== 關鍵代碼 end ==== } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }
終於走到底了,能夠看到關鍵代碼中的try-catch block代碼塊中,調用了本次執行任務的run
方法。
// ==== 關鍵代碼 start ==== try { // 很簡潔明瞭,調用了任務的run方法 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); } // ==== 關鍵代碼 end ====
能夠看到捕捉了異常以後,會再向外拋出,只不過再finally block 中有個afterExecute()
方法,彷佛在這裏是能夠處理這個異常信息的,進去看看
protected void afterExecute(Runnable r, Throwable t) { }
能夠看到ThreadPoolExecutor#afterExecute()
方法中,是什麼都沒作的,看來是讓使用者經過override這個方法來定製化任務執行以後的邏輯,其中能夠包括異常處理。
那麼這個異常究竟是拋到哪裏去了呢。我在一個大佬的文章找到了hotSpot JVM處理線程異常的邏輯,
if (!destroy_vm || JDK_Version::is_jdk12x_version()) { // JSR-166: change call from from ThreadGroup.uncaughtException to // java.lang.Thread.dispatchUncaughtException if (uncaught_exception.not_null()) { //若是有未捕獲的異常 Handle group(this, java_lang_Thread::threadGroup(threadObj())); { KlassHandle recvrKlass(THREAD, threadObj->klass()); CallInfo callinfo; KlassHandle thread_klass(THREAD, SystemDictionary::Thread_klass()); /* 這裏相似一個方法表,實際就會去調用Thread#dispatchUncaughtException方法 template(dispatchUncaughtException_name, "dispatchUncaughtException") */ LinkResolver::resolve_virtual_call(callinfo, threadObj, recvrKlass, thread_klass, vmSymbols::dispatchUncaughtException_name(), vmSymbols::throwable_void_signature(), KlassHandle(), false, false, THREAD); CLEAR_PENDING_EXCEPTION; methodHandle method = callinfo.selected_method(); if (method.not_null()) { JavaValue result(T_VOID); JavaCalls::call_virtual(&result, threadObj, thread_klass, vmSymbols::dispatchUncaughtException_name(), vmSymbols::throwable_void_signature(), uncaught_exception, THREAD); } else { KlassHandle thread_group(THREAD, SystemDictionary::ThreadGroup_klass()); JavaValue result(T_VOID); JavaCalls::call_virtual(&result, group, thread_group, vmSymbols::uncaughtException_name(), vmSymbols::thread_throwable_void_signature(), threadObj, // Arg 1 uncaught_exception, // Arg 2 THREAD); } if (HAS_PENDING_EXCEPTION) { ResourceMark rm(this); jio_fprintf(defaultStream::error_stream(), "\nException: %s thrown from the UncaughtExceptionHandler" " in thread \"%s\"\n", pending_exception()->klass()->external_name(), get_thread_name()); CLEAR_PENDING_EXCEPTION; } } }
代碼是C寫的,有興趣能夠去全文,根據英文註釋能稍微看懂一點
http://hg.openjdk.java.net/jd...
能夠看到這裏最終會去調用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); }
/** * 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. * */ 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)) { //能夠看到會打到System.err裏面 System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err); } } }
jdk的註釋也說明的很清楚了,當一個線程拋出了一個未捕獲的異常,JVM會去調用這個方法。若是當前線程沒有聲明UncaughtExceptionHandler
成員變量而且重寫uncaughtException
方法的時候,就會看線程所屬的線程組(若是有線程組的話)有沒有這個類,沒有就會打到System.err
裏面。
IBM這篇文章也提倡咱們使用ThreadGroup
提供的 uncaughtException
處理程序來在線程異常終止時進行檢測。
https://www.ibm.com/developer...
從上述源碼分析中能夠看到,對於本篇的異常「被吃掉」的問題,有如下幾種方法
線程或者線程組對象設置UncaughtExceptionHandler成員變量
Thread t = new Thread(r); t.setUncaughtExceptionHandler( (t1, e) -> LOGGER.error(t1 + " throws exception: " + e)); return t;
afterExecute
方法。本篇雖然是提出問題的解決方法,但主旨仍是分析源碼,瞭解了整個過程當中異常的通過的流程,但願能對您產生幫助。