线程池中抛出的异常,主线程可以catch到吗

印象中,线程池抛出的异常主线程可以catch到,但前段时间碰到个问题,在系统出现异常时,由于线程池任务中没有catch异常的代码,主线程虽有catch异常代码,但却没有catch到异常,导致排查问题比较费劲。

故对此处进行研究,并记录。

一、JVM异常处理

先来看一下jvm对未捕获的异常如何处理

1     @Test
2     public void testException() {
3         int a = 1 / 0;
4     }

上述代码执行结果如下:

当除数为0时,我们并没有去捕获异常,但控制台却打印了异常栈信息。

JVM异常处理机制主要包括以下几个步骤:

  1. 异常抛出:当程序执行过程中发生异常时,会创建一个对应的异常对象,并将其抛出。

  2. 异常捕获:在代码中使用try-catch语句块来捕获异常。catch块中的代码会在异常发生时被执行,用于处理异常情况。

  3. 异常传播:如果异常在当前方法中没有被捕获,它会被传播到调用该方法的地方。这个过程会一直持续到找到合适的异常处理器或者到达程序的顶层,如果没有找到合适的异常处理器,程序将会终止并打印异常信息。

  4. 异常处理:当异常被捕获时,catch块中的代码会被执行。在catch块中,可以对异常进行处理,比如打印异常信息、记录日志、进行补救操作等。

  5. 异常链:在异常处理过程中,可以使用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 &quot;default&quot; 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      *         (&quot;setDefaultUncaughtExceptionHandler&quot;)</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到

 

posted @ 2024-05-12 14:08  开坦克的舒克  阅读(39)  评论(0编辑  收藏  举报