印象中,线程池抛出的异常主线程可以catch到,但前段时间碰到个问题,在系统出现异常时,由于线程池任务中没有catch异常的代码,主线程虽有catch异常代码,但却没有catch到异常,导致排查问题比较费劲。
故对此处进行研究,并记录。
一、JVM异常处理
先来看一下jvm对未捕获的异常如何处理
1 @Test 2 public void testException() { 3 int a = 1 / 0; 4 }
上述代码执行结果如下:
当除数为0时,我们并没有去捕获异常,但控制台却打印了异常栈信息。
JVM异常处理机制主要包括以下几个步骤:
-
异常抛出:当程序执行过程中发生异常时,会创建一个对应的异常对象,并将其抛出。
-
异常捕获:在代码中使用try-catch语句块来捕获异常。catch块中的代码会在异常发生时被执行,用于处理异常情况。
-
异常传播:如果异常在当前方法中没有被捕获,它会被传播到调用该方法的地方。这个过程会一直持续到找到合适的异常处理器或者到达程序的顶层,如果没有找到合适的异常处理器,程序将会终止并打印异常信息。
-
异常处理:当异常被捕获时,catch块中的代码会被执行。在catch块中,可以对异常进行处理,比如打印异常信息、记录日志、进行补救操作等。
-
异常链:在异常处理过程中,可以使用throw关键字将一个异常抛出,并将其作为另一个异常的原因。这样可以形成异常链,方便定位和追踪异常的根本原因。
JVM会根据异常处理机制来处理异常。代码中,异常会被抛到调用栈的上一层,直到找到合适的异常处理器来处理异常,如果没有找到合适的异常处理器,程序会终止执行并打印异常信息。
二、子线程抛出异常
1 @Test 2 public void testThreadException() { 3 try { 4 Thread thread = new Thread(() -> { 5 System.out.println("子线程测试"); 6 throw new RuntimeException("系统异常"); 7 }); 8 thread.start(); 9 } catch (Exception e) { 10 System.out.println("捕获异常:" + e); 11 } 12 }
执行结果如下:
考虑到子线程是异步处理的,有可能当执行完catch逻辑后,子线程才去执行,故对上述代码做如下变更:
1 @Test 2 public void testThreadException1() { 3 try { 4 Thread thread = new Thread(() -> { 5 System.out.println("子线程测试"); 6 throw new RuntimeException("系统异常"); 7 }); 8 thread.start(); 9 Thread.sleep(3000); 10 } catch (Exception e) { 11 System.out.println("捕获异常:" + e); 12 } 13 }
执行结果不变,即主线程无法catch子线程抛出的异常。分析原因如下:
1 public synchronized void start() { 2 /** 3 * This method is not invoked for the main method thread or "system" 4 * group threads created/set up by the VM. Any new functionality added 5 * to this method in the future may have to also be added to the VM. 6 * 7 * A zero status value corresponds to state "NEW". 8 */ 9 if (threadStatus != 0) 10 throw new IllegalThreadStateException(); 11 12 /* Notify the group that this thread is about to be started 13 * so that it can be added to the group's list of threads 14 * and the group's unstarted count can be decremented. */ 15 group.add(this); 16 17 boolean started = false; 18 try { 19 start0(); 20 started = true; 21 } finally { 22 try { 23 if (!started) { 24 group.threadStartFailed(this); 25 } 26 } catch (Throwable ignore) { 27 /* do nothing. If start0 threw a Throwable then 28 it will be passed up the call stack */ 29 } 30 } 31 } 32 33 private native void start0();
上述代码为Thread类源码,当调用thread.start() 时,会执行上述方法,可以看到,该方法会调用start0() 方法。start0()方法为本地方法,该方法会调用Thread类中的run()方法,如下所示:
1 @Override 2 public void run() { 3 if (target != null) { 4 target.run(); 5 } 6 }
run() 方法中,target为Runnable类型,即为上述例子中,子线程的任务体。Thread类的run()方法执行target.run(),在出现异常时,由于任务中无异常处理代码,Thread类的run()方法中也无异常处理代码,异常会由JVM来处理。
1 /** 2 * Dispatch an uncaught exception to the handler. This method is 3 * intended to be called only by the JVM. 4 */ 5 private void dispatchUncaughtException(Throwable e) { 6 getUncaughtExceptionHandler().uncaughtException(this, e); 7 }
JVM会调用Thread类中的上述方法来处理异常,注意,该方法为private修饰。
此处对Thread源码进行进一步分析,Thread类中定义了UncaughtExceptionHandler接口来处理未捕获异常。用户可自行实现该接口定义异常处理方式,并通过setDefaultUncaughtExceptionHandler()或setUncaughtExceptionHandler()赋值给变量defaultUncaughtExceptionHandler或uncaughtExceptionHandler。ThreadGroup类也实现了Thread.UncaughtExceptionHandler接口。
1 @FunctionalInterface 2 public interface UncaughtExceptionHandler { 3 /** 4 * Method invoked when the given thread terminates due to the 5 * given uncaught exception. 6 * <p>Any exception thrown by this method will be ignored by the 7 * Java Virtual Machine. 8 * @param t the thread 9 * @param e the exception 10 */ 11 void uncaughtException(Thread t, Throwable e); 12 } 13 14 // null unless explicitly set 15 private volatile UncaughtExceptionHandler uncaughtExceptionHandler; 16 17 // null unless explicitly set 18 private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler; 19 20 /** 21 * Set the default handler invoked when a thread abruptly terminates 22 * due to an uncaught exception, and no other handler has been defined 23 * for that thread. 24 * 25 * <p>Uncaught exception handling is controlled first by the thread, then 26 * by the thread's {@link ThreadGroup} object and finally by the default 27 * uncaught exception handler. If the thread does not have an explicit 28 * uncaught exception handler set, and the thread's thread group 29 * (including parent thread groups) does not specialize its 30 * <tt>uncaughtException</tt> method, then the default handler's 31 * <tt>uncaughtException</tt> method will be invoked. 32 * <p>By setting the default uncaught exception handler, an application 33 * can change the way in which uncaught exceptions are handled (such as 34 * logging to a specific device, or file) for those threads that would 35 * already accept whatever "default" behavior the system 36 * provided. 37 * 38 * <p>Note that the default uncaught exception handler should not usually 39 * defer to the thread's <tt>ThreadGroup</tt> object, as that could cause 40 * infinite recursion. 41 * 42 * @param eh the object to use as the default uncaught exception handler. 43 * If <tt>null</tt> then there is no default handler. 44 * 45 * @throws SecurityException if a security manager is present and it 46 * denies <tt>{@link RuntimePermission} 47 * ("setDefaultUncaughtExceptionHandler")</tt> 48 * 49 * @see #setUncaughtExceptionHandler 50 * @see #getUncaughtExceptionHandler 51 * @see ThreadGroup#uncaughtException 52 * @since 1.5 53 */ 54 public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) { 55 SecurityManager sm = System.getSecurityManager(); 56 if (sm != null) { 57 sm.checkPermission( 58 new RuntimePermission("setDefaultUncaughtExceptionHandler") 59 ); 60 } 61 62 defaultUncaughtExceptionHandler = eh; 63 } 64 65 /** 66 * Returns the default handler invoked when a thread abruptly terminates 67 * due to an uncaught exception. If the returned value is <tt>null</tt>, 68 * there is no default. 69 * @since 1.5 70 * @see #setDefaultUncaughtExceptionHandler 71 * @return the default uncaught exception handler for all threads 72 */ 73 public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){ 74 return defaultUncaughtExceptionHandler; 75 } 76 77 /** 78 * Returns the handler invoked when this thread abruptly terminates 79 * due to an uncaught exception. If this thread has not had an 80 * uncaught exception handler explicitly set then this thread's 81 * <tt>ThreadGroup</tt> object is returned, unless this thread 82 * has terminated, in which case <tt>null</tt> is returned. 83 * @since 1.5 84 * @return the uncaught exception handler for this thread 85 */ 86 public UncaughtExceptionHandler getUncaughtExceptionHandler() { 87 return uncaughtExceptionHandler != null ? 88 uncaughtExceptionHandler : group; 89 } 90 91 /** 92 * Set the handler invoked when this thread abruptly terminates 93 * due to an uncaught exception. 94 * <p>A thread can take full control of how it responds to uncaught 95 * exceptions by having its uncaught exception handler explicitly set. 96 * If no such handler is set then the thread's <tt>ThreadGroup</tt> 97 * object acts as its handler. 98 * @param eh the object to use as this thread's uncaught exception 99 * handler. If <tt>null</tt> then this thread has no explicit handler. 100 * @throws SecurityException if the current thread is not allowed to 101 * modify this thread. 102 * @see #setDefaultUncaughtExceptionHandler 103 * @see ThreadGroup#uncaughtException 104 * @since 1.5 105 */ 106 public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) { 107 checkAccess(); 108 uncaughtExceptionHandler = eh; 109 }
dispatchUncaughtException()方法首先会调用getUncaughtExceptionHandler()获取未捕获异常处理器,由于没有自定义异常处理器,故返回该线程的线程组对象group
1 public UncaughtExceptionHandler getUncaughtExceptionHandler() { 2 return uncaughtExceptionHandler != null ? 3 uncaughtExceptionHandler : group; 4 }
然后调用ThreadGroup类中的uncaughtException()方法
1 public void uncaughtException(Thread t, Throwable e) { 2 if (parent != null) { 3 parent.uncaughtException(t, e); 4 } else { 5 Thread.UncaughtExceptionHandler ueh = 6 Thread.getDefaultUncaughtExceptionHandler(); 7 if (ueh != null) { 8 ueh.uncaughtException(t, e); 9 } else if (!(e instanceof ThreadDeath)) { 10 System.err.print("Exception in thread \"" 11 + t.getName() + "\" "); 12 e.printStackTrace(System.err); 13 } 14 } 15 }
可以看出,代码执行后控制台打印的异常为uncaughtException()方法打印
三、线程池中的子线程抛出异常
以ThreadPoolExecutor为例进行实验,该类提供了execute()方法,并继承AbstractExecutorService类的几个submit方法。区别是execute()直接执行任务,无返回值。submit方法执行任务,并返回Future对象。
1、execute()方法提交任务
如下图代码所示,创建一个线程池,线程池中的线程数为10,执行子线程任务,并在子线程中抛出异常
1 @Test 2 public void testThreadPoolException1() { 3 try { 4 ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(10); 5 threadPoolExecutor.execute(() -> { 6 System.out.println("线程池测试"); 7 throw new RuntimeException("系统异常"); 8 }); 9 } catch (Exception e) { 10 System.out.println("捕获异常:" + e); 11 } 12 }
代码运行结果如下:
主线程同样没有捕获到异常。关于线程池源码,此处不再进行分析,后续有时间了单独分析一下。大家感兴趣可以自己去搜资料。
此处execute()方法会调用addWorker()方法,addwork()会调用t.start()方法,详细调用链如下:
execute() -> addWork() -> t.start() -> Thread类 start() -> Thread类 start0() -> Thread类 run() -> ThreadPoolExecutor.Worker中run() -> ThreadPoolExecutor中 runWorker()
1 final void runWorker(Worker w) { 2 Thread wt = Thread.currentThread(); 3 Runnable task = w.firstTask; 4 w.firstTask = null; 5 w.unlock(); // allow interrupts 6 boolean completedAbruptly = true; 7 try { 8 while (task != null || (task = getTask()) != null) { 9 w.lock(); 10 // If pool is stopping, ensure thread is interrupted; 11 // if not, ensure thread is not interrupted. This 12 // requires a recheck in second case to deal with 13 // shutdownNow race while clearing interrupt 14 if ((runStateAtLeast(ctl.get(), STOP) || 15 (Thread.interrupted() && 16 runStateAtLeast(ctl.get(), STOP))) && 17 !wt.isInterrupted()) 18 wt.interrupt(); 19 try { 20 beforeExecute(wt, task); 21 Throwable thrown = null; 22 try { 23 task.run(); 24 } catch (RuntimeException x) { 25 thrown = x; throw x; 26 } catch (Error x) { 27 thrown = x; throw x; 28 } catch (Throwable x) { 29 thrown = x; throw new Error(x); 30 } finally { 31 afterExecute(task, thrown); 32 } 33 } finally { 34 task = null; 35 w.completedTasks++; 36 w.unlock(); 37 } 38 } 39 completedAbruptly = false; 40 } finally { 41 processWorkerExit(w, completedAbruptly); 42 } 43 }
runWorker()方法执行task.run()时,会执行任务方法,任务抛出异常后,会被catch到并再次抛出。后续流程与Thread中的未捕获异常处理逻辑相同,即调用dispatchUncaughtException()方法处理异常
2、submit方法提交任务
1 @Test 2 public void testThreadPoolException2() { 3 try { 4 ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(10); 5 threadPoolExecutor.submit(() -> { 6 System.out.println("线程池测试"); 7 throw new RuntimeException("系统异常"); 8 }); 9 } catch (Exception e) { 10 System.out.println("捕获异常:" + e); 11 } 12 }
执行结果如下:
主线程不会捕获到异常,同时,控制台也不会打印异常。继续执行下列代码:
1 @Test 2 public void testThreadPoolException3() { 3 try { 4 ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(10); 5 Future future = threadPoolExecutor.submit(() -> { 6 System.out.println("线程池测试"); 7 throw new RuntimeException("系统异常"); 8 }); 9 future.get(); 10 } catch (Exception e) { 11 System.out.println("捕获异常:" + e); 12 } 13 }
执行结果如下,主线程捕获到了异常。
通过源码进行分析:
1 public Future<?> submit(Runnable task) { 2 if (task == null) throw new NullPointerException(); 3 RunnableFuture<Void> ftask = newTaskFor(task, null); 4 execute(ftask); 5 return ftask; 6 }
执行submit方法,submit方法体在AbstractExecutorService类中。首先创建RunnableFuture,执行execute()方法,最后返回future。
1 protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { 2 return new FutureTask<T>(runnable, value); 3 }
RunnableFuture接口实现类为FutureTask。execute(ftask)方法执行过程与上述1中执行过程一致,区别在于runWorker(Worker w)方法执行task.run()时,执行的是FutureTask类的run()方法。
1 public void run() { 2 if (state != NEW || 3 !UNSAFE.compareAndSwapObject(this, runnerOffset, 4 null, Thread.currentThread())) 5 return; 6 try { 7 Callable<V> c = callable; 8 if (c != null && state == NEW) { 9 V result; 10 boolean ran; 11 try { 12 result = c.call(); 13 ran = true; 14 } catch (Throwable ex) { 15 result = null; 16 ran = false; 17 setException(ex); 18 } 19 if (ran) 20 set(result); 21 } 22 } finally { 23 // runner must be non-null until state is settled to 24 // prevent concurrent calls to run() 25 runner = null; 26 // state must be re-read after nulling runner to prevent 27 // leaked interrupts 28 int s = state; 29 if (s >= INTERRUPTING) 30 handlePossibleCancellationInterrupt(s); 31 } 32 }
上述源码第12行会执行任务,任务抛出异常后,会被catch并调用setException(ex)方法。
1 protected void setException(Throwable t) { 2 if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { 3 outcome = t; 4 UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state 5 finishCompletion(); 6 } 7 }
该方法将异常赋值给变量outcome。故通过submit()方法提交任务后,线程池中的异常没有被主线程捕获,也没有在控制台打印异常。再来看future.get()的执行逻辑。
1 public V get() throws InterruptedException, ExecutionException { 2 int s = state; 3 if (s <= COMPLETING) 4 s = awaitDone(false, 0L); 5 return report(s); 6 }
通过状态判断任务是否执行完毕,执行完毕后调用report()方法。
1 private V report(int s) throws ExecutionException { 2 Object x = outcome; 3 if (s == NORMAL) 4 return (V)x; 5 if (s >= CANCELLED) 6 throw new CancellationException(); 7 throw new ExecutionException((Throwable)x); 8 }
report方法会抛出变量outcome中的异常
由于future.get()为同步方法,故异常会抛到主线程中
3、CompletableFuture
1 @Test 2 public void testThreadPoolException4() { 3 try { 4 CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { 5 System.out.println("线程池测试"); 6 throw new RuntimeException("系统异常"); 7 }); 8 } catch (Exception e) { 9 System.out.println("捕获异常:" + e); 10 } 11 } 12 13 @Test 14 public void testThreadPoolException5() { 15 try { 16 CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { 17 System.out.println("线程池测试"); 18 throw new RuntimeException("系统异常"); 19 }); 20 completableFuture.join(); 21 } catch (Exception e) { 22 System.out.println("捕获异常:" + e); 23 } 24 } 25 26 @Test 27 public void testThreadPoolException6() { 28 try { 29 CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { 30 System.out.println("线程池测试"); 31 throw new RuntimeException("系统异常"); 32 }); 33 completableFuture.get(); 34 } catch (Exception e) { 35 System.out.println("捕获异常:" + e); 36 } 37 }
testThreadPoolException4中主线程不会捕获到异常,testThreadPoolException5和testThreadPoolException6中,主线程可捕获到异常,在此不做进一步分析。
四、总结
综上所述:
1、直接创建子线程,若子线程中抛出异常,主线程无法catch到。虽然Thread类会默认打印异常栈到控制台,但生产环境中建议在子线程中增加捕获异常逻辑,并打印日志。
2、线程池中的异常,主线程无法catch到。建议增加捕获异常逻辑,并打印日志。
3、使用get()、join()等方法获取线程执行结果时,线程池中抛出的异常,主线程可以catch到
标签:thread,Thread,池中,线程,catch,null,异常 From: https://www.cnblogs.com/javaXRG/p/18187773