Java 线程池内部任务出异常后,如何知道是哪个线程出了异常?

Sherwin.Wei Lv7

Java 线程池内部任务出异常后,如何知道是哪个线程出了异常?

重点回答

默认情况下,线程池不会直接报告哪个线程发生了异常,但是可以采取以下几种方法:

1)**自定义线程池的 ThreadFactory**:

  • 通过自定义 ThreadFactory,为每个线程设置一个异常处理器(UncaughtExceptionHandler),在其中记录发生异常的线程信息。

2)**使用 Future**:

  • 提交任务时使用 submit() 方法,而不是 execute(),这样可以通过 Future 对象捕获并检查任务的执行结果和异常。

3)任务内部手动捕获异常并记录

  • 在任务的 run() 方法内部,使用 try-catch 结构捕获异常,并记录或处理异常,同时记录线程信息。

扩展知识

方案落地

使用 ThreadFactory 和 UncaughtExceptionHandler

通过自定义 ThreadFactory,为每个线程设置一个 UncaughtExceptionHandler,记录异常信息。

1
2
3
4
5
6
7
8
9
10
11

public class CustomThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("面试鸭Thread " + t.getName() + " threw exception: " + e);
});
return thread;
}
}

使用 Future 捕获异常

通过 submit() 提交任务,使用 Future 获取任务执行结果,如果任务抛出异常,可以通过 Future.get() 捕获并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.*;

public class ThreadPoolWithFuture {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);

Future<?> future = executor.submit(() -> {
throw new RuntimeException("Exception in thread");
});

try {
future.get(); // 可以获取报错
} catch (InterruptedException | ExecutionException e) {
System.out.println("Task threw exception: " + e.getCause());
}

executor.shutdown();
}
}

如果任务抛出异常,get() 方法会抛出 ExecutionException,其中包含任务的异常信息。

任务内部捕获异常并记录

在任务的 run() 方法内部手动捕获异常,并记录异常及线程信息。

1
2
3
4
5
6
7
8
9
10
11
public class TaskWithExceptionHandling implements Runnable {
@Override
public void run() {
try {
// do sth
throw new RuntimeException("Exception in task");
} catch (Exception e) {
System.out.println("手动打印:Exception caught in thread " + Thread.currentThread().getName() + ": " + e);
}
}
}

如果线程池中的线程在执行任务的时候,抛异常了,会怎么样?

一共有两种情况:

  • 如果使用 execute() 提交任务,任务执行时抛出未捕获异常,线程会被移除,线程池会创建新线程;
  • 如果使用 submit() 提交任务,任务执行时抛出未捕获异常,异常会封装在 ExecutionException 中返回,不会抛出,且不会创建新线程。

看下execute()相关源码,最终会调用 runWorker 方法:

image.png

任务执行逻辑就是 task.run() ,可以看到它被 try catch finally包裹,异常被扔到了 afterExecute 中,并且也继续被抛了出来。

而这一层外面,还有个try finally,所以异常的抛出打破了 while 循环,最终会执行 processWorkerExit 方法。

我们来看下这个方法,其实逻辑很简单,把这个线程废了,然后新建一个线程替换之

image.png

移除了引用就等于销毁了,后续会被 GC 了。

所以如果一个任务执行一半就抛出异常,并且你没有自行处理这个异常,那么这个任务就这样戛然而止了,后面也不会有线程继续执行剩下的逻辑,所以要自行捕获和处理业务异常。

实际上 submit 最终也会调用 execute,差别就在于包了一层 RunnableFuture

image.png

那么 submit 也调用了 execute 为什么不会创建新线程呢?

门道就在 RunnableFuture,从下面代码可以看到 RunnableFuture 的实现类 FutureTask 中的 run 方法有个setException(ex); 逻辑,就是它把异常给 catch 住了,没有继续往上抛,所以没有触发移除线程和新建线程的操作。

image.png
Comments