Java线程池UncaughtExceptionHandler无效?可能是使用方式不对
背景
在业务处理中,使用了线程池来提交任务执行,但是今天修改了一小段代码,发现任务未正确执行。而且看了相关日志,也并未打印结果。
源码简化版如下: 首先,自定义了一个线程池
public class NamedThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final ThreadGroup group;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public NamedThreadFactory(String namePrefix) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
this.namePrefix = namePrefix + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
t.setUncaughtExceptionHandler(new ThreadUncaugthExceptionHandler());
return t;
}
private class ThreadUncaugthExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
logger.error("uncaughtException thead name:{}, msg:{}", t.getName(), e.getMessage(), e);
}
}
}
线程池A如下所示
ThreadPoolExecutor EXECUTOR_A =
new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(100),
new NamedThreadFactory("AService-"));
待执行任务
EXECUTOR_A.submit(() -> {
// 处理step1
......
// 以下是本次新增代码
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
LocalDate now = LocalDate.now(zoneId);
LocalDate endTime = now.plus(1, ChronoUnit.YEARS);
//final变量DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String endTIme = DATE_TIME_FORMATTER.format(endTime);
// 调用B的处理方法
});
对于上述新增的代码,会报以下异常
java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: HourOfDay
这是因为希望格式化的是yyyy-MM-dd HH:mm:ss格式,我使用的是LocalDate,实际应该使用LocalDateTime才对。
解析
从背景中可以看到新增的代码由于书写错误,会报异常。
同时由于exeuctor提交的Runnable任务中缺少try-catch相应处理,那么该任务会执行失败。但是这里有一个奇怪的地方,明明给线程池自定了ThreadFactory,并且指定了UncaughtExceptionHandler,里面应该会打印错误日志才对。
可是翻遍了日志,却一点没有找到。 到这里有一些朋友可能已经知道问题了,问题的关键就在于任务提交的方式,也就是submit和execute的差异。
概况一下,在Executor框架中,线程池提供了两个方法用于提交任务:execute()和submit()。这两个方法的主要区别如下:
- execute()方法:
- 用于提交不需要返回值的任务,即Runnable类型的任务。
- execute()方法将任务提交给线程池后,将立即返回,而不等待任务执行完成或返回结果。
- 如果任务内部发生异常,线程池会捕获并抛出异常。
- submit()方法:
- 用于提交需要返回值的任务,即Callable类型的任务,也可以执行Runnable,会以Void作为返回类型。
- submit()方法将任务提交给线程池后,返回一个Future对象,可以使用该对象的get()方法获取任务执行的结果。
- 如果任务内部发生异常,线程池会将异常封装在ExecutionException中,通过Future对象的get()方法处理抛出的ExecutionException。
对于execute方法中的异常处理,可以查看以下代码,红框中是对于RuntimeException直接抛出。
java.util.concurrent.ThreadPoolExecutor#runWorker

而对于submit方法来说,任务提交的时候,会创建一个FutureTask。


FutureTask的run方法处理如下

在异常情况下,将异常赋值给了outcome。

而当我们调用了Future.get()方法时,


综上分析,如果是execute方式提交任务,异常会直接抛出,最终进入到自定义的UncaughtExceptionHandler。如果是submit方式提交任务,异常只会在Future.get()方法时抛出,如果并没有调用get方法,那么是不会感知到异常的。此时也就是本文中的情况,就无法看到自定义的UncaughtExceptionHandler打印的日志了。
总结
推荐的处理方式
- 推荐try-catch对线程任务进行异常捕获
- 推荐自定义ThreadFacory,并自定义UncaughtExceptionHandler进行异常打印,避免有一些异常捕获遗漏的情况。当然此场景下,一定要区分submit和execute任务提交方式
扩展
除了使用上面的ThreadFactory方式外,还有其他几个方式。
包装运行任务
public static class CatchingExceptionRunnable implements Runnable {
private final Runnable delegate;
public CatchingExceptionRunnable(Runnable delegate) {
this.delegate = delegate;
}
@Override
public void run() {
try {
delegate.run();
} catch (RuntimeException e) {
// 异常处理逻辑
}
}
}
适用场景
- 同一个线程池可能在处理不同的任务,有的适用于默认ThreadPool统一的UncaughtExceptionHandler,而有的任务需要特殊处理。
- 在个别场景下,我们无法给使用的线程池通过指定ThreadFactory的UncaughtExceptionHandler进行异常处理,只能从任务本身处理。
覆盖ThreadPoolExecutor的afterExecute方法
java.util.concurrent.ThreadPoolExecutor#afterExecute
Method invoked upon completion of execution of the given Runnable(该方法会在任务执行完成后被调用)
该方法是在ThreadPoolExecutor的runWorker的finaly方法中触发的。 示例代码
public static class MonitoringThreadPoolExecutor extends ThreadPoolExecutor {
public MonitoringThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if(t != null){
System.out.println("Exception message: " + t.getMessage());
}
}
}
但是execute和submit方式还是有区别。 对于execute方法,此时异常就是任务抛出的异常。但是对于submit方式,此时异常时null。具体可见下图。


如果是上述代码的实现,此时通过submit提交的任务发生异常时,仍然是无法解析到的。如果要解析到,可以参照JDK给的解释和示例
public static class MonitoringThreadPoolExecutor extends ThreadPoolExecutor {
public MonitoringThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
Object result = ((Future<?>)r).get();
} catch (CancellationException ce) {
t = ce;
} catch ( ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if(t != null){
System.out.println("Exception message: " + t.getMessage());
}
}
}
本质还是前面提到的在方法中使用Future.get将异常信息得到再做处理。
转载自:https://juejin.cn/post/7270046196643102777