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()。这两个方法的主要区别如下:

  1. execute()方法:
    • 用于提交不需要返回值的任务,即Runnable类型的任务。
    • execute()方法将任务提交给线程池后,将立即返回,而不等待任务执行完成或返回结果。
    • 如果任务内部发生异常,线程池会捕获并抛出异常。
  2. submit()方法:
    • 用于提交需要返回值的任务,即Callable类型的任务,也可以执行Runnable,会以Void作为返回类型。
    • submit()方法将任务提交给线程池后,返回一个Future对象,可以使用该对象的get()方法获取任务执行的结果。
    • 如果任务内部发生异常,线程池会将异常封装在ExecutionException中,通过Future对象的get()方法处理抛出的ExecutionException。

对于execute方法中的异常处理,可以查看以下代码,红框中是对于RuntimeException直接抛出。

java.util.concurrent.ThreadPoolExecutor#runWorker
Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第1张图片

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

Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第2张图片Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第3张图片

FutureTask的run方法处理如下

Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第4张图片

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

Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第5张图片

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

Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第6张图片

Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第7张图片

综上分析,如果是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) {
            // 异常处理逻辑
        }
    }
}

适用场景

  1. 同一个线程池可能在处理不同的任务,有的适用于默认ThreadPool统一的UncaughtExceptionHandler,而有的任务需要特殊处理。
  2. 在个别场景下,我们无法给使用的线程池通过指定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。具体可见下图。

Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第8张图片

Java线程池UncaughtExceptionHandler无效?可能是使用方式不对_第9张图片

如果是上述代码的实现,此时通过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将异常信息得到再做处理。

你可能感兴趣的:(java)