在业务处理中,使用了线程池来提交任务执行,但是今天修改了一小段代码,发现任务未正确执行。而且看了相关日志,也并未打印结果。
源码简化版如下:
首先,自定义了一个线程池
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方法中的异常处理,可以查看以下代码,红框中是对于RuntimeException直接抛出。
而对于submit方法来说,任务提交的时候,会创建一个FutureTask。
FutureTask的run方法处理如下
在异常情况下,将异常赋值给了outcome。
而当我们调用了Future.get()方法时,
综上分析,如果是execute方式提交任务,异常会直接抛出,最终进入到自定义的UncaughtExceptionHandler。如果是submit方式提交任务,异常只会在Future.get()方法时抛出,如果并没有调用get方法,那么是不会感知到异常的。此时也就是本文中的情况,就无法看到自定义的UncaughtExceptionHandler打印的日志了。
推荐的处理方式
除了使用上面的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) {
// 异常处理逻辑
}
}
}
适用场景
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将异常信息得到再做处理。