背景
在业务处理中,使用了线程池来提交任务执行,但是今天修改了一小段代码,发现任务未正确执行。而且看了相关日志,也并未打印结果。
源码简化版如下:
首先,自定义了一个线程池
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-";}@Overridepublic 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 {@Overridepublic 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;}@Overridepublic 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);}@Overrideprotected 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);}@Overrideprotected 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将异常信息得到再做处理。