背景
在业务处理中,使用了线程池来提交任务执行,但是今天修改了一小段代码,发现任务未正确执行。而且看了相关日志,也并未打印结果。
源码简化版如下:
首先,自定义了一个线程池
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任务提交方式