ThreadPoolExecutor异常处理

前言

本文所涉及的逻辑以及原理均基于JDK1.8。

在各种鼓吹高并发,高性能的年代,不会用线程池都不好意思说自己是会搬砖的程序猿。但是使用线程池有许多坑,比如本文讨论的处理线程池处理任务的异常问题。看似简单,好像找到了更好的处理方式,其实还有更深的坑等着你。

Java的J.U.C提供了几个方便管理线程的线程池类,比如最常用的ThreadPoolExecutor,还有用于执行计划等方面的ScheduledThreadPoolExecutor,还有分而治之的小偷ForkJoinPool。在使用这些线程池执行任务时难免会发生不可预知的异常,在这些异常发生的时候我们的程序应该如何处理?我们的处理方式是否能按我们的预期执行?如何统计这些异常?这些你真的清楚吗? 


简单处理模式

在讨论ThreadPoolExecutor处理任务异常的时候,可能有的同学会想到直接使用try...cache将整个任务包裹住,然后在cache中进行异常的处理。就像下面这样:

 executorService.submit(() -> {
            try {
                // TODO 
            } catch (Exception e) {
                // TODO 
            }
        });

这样使用try...cache包围住代码块后出现异常就能在catch块中处理了。这样处理确实是可以解决问题,如果是处理特定的问题这样处理没有问题,但是处理一些通用的异常情况(比如:统计线程池异常任务数)这样就显得代码冗余了。

在线程池执行任务的java.util.concurrent.ThreadPoolExecutor#runWorker方法执行任务时是会使用try...catch包围任务的。

                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }

通过上面的代码我们可以看到被catch的异常是会被传入到afterExecute这个方法中。protected void afterExecute(Runnable r, Throwable t) 方法是线程池的一个hook方法,在任务执行完成后会去调用,所以我们可以使用线程池的hook方法去处理异常。了解更多关于线程池hook方法介绍

 

Hook方法处理模式

ThreadPoolExecutorService中提供了几个执行任务时会调用的hook方法,上面提到的protected void afterExecute(Runnable r, Throwable t) 方法就是其中的一个。在这个hook方法中,我们判断Throwable参数是否为空就能知道是否抛出了异常,然后作出相应的处理就可以了。

看到这里大家可能都觉得在afterExecute钩子方法中处理后程序就会按我们设想的方式运行了。如果是使用ThreadPoolExecutor进行任务提交,发生异常后在afterExecute中接收的Throwable是null。可能很多不熟悉J.U.C的小伙伴会一脸懵逼,JDK源码java.util.concurrent.ThreadPoolExecutor#runWorker就是会catch住任务执行的异常,
为什么它会是null?

要理清楚里面的缘由要从任务的提交说起。
使用ThreadPoolExecutor的submit或execut提交任务的时候会将我们提交的Callable或Runnable封装成一个FutureTask。也就是说线程池执行的任务就是这个被封装的FutureTask。FutureTask实现了Runnable接口,所以它也有一个run方法。在这个方法中它会去执行我们提交的Runnable或Callable。

try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }

我们只用看其中一部分逻辑就能清楚为什么线程池的钩子方法没有接收到异常。在 FutureTask的run方法中,我们提交给线程池的Runnable或Callable是执行在try..catch中的,抛出的异常被FutureTask吃掉放到成员变量outcome中存储,在调用Future.get时会被抛出。

异常会被吃掉之前说的hook是不是就是鸡肋了呢?JDK的把这个问题的处理方式放在了文档中。

 * class ExtendedExecutor extends ThreadPoolExecutor {
     *   // ...
     *   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(); // ignore/reset
     *       }
     *     }
     *     if (t != null)
     *       System.out.println(t);
     *   }

当然如果你觉得这样不够优雅,也可以重写submit/execute方法,不要将任务封装成FutureTask,使用自己重写的Future类。

 

---

希望对你有帮助。

转载注明出处:https://blog.csdn.net/LCBUSHIHAHA/article/details/111397261

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值