项目组内的老代码,遇到的问题是:使用周期性线程池定时调度执行某任务,非常偶然的,某次调度失败,并且后续定时调度任务停止;
排查日志,排除了OOM后,未见任何异常;很明显是当前任务执行时抛出了异常,并且未捕获该异常;但是最蛋疼的就是当前的代码实现压根看不出该任务抛出了什么异常,也没有任何辅助信息。。。。。。很想把当时写这段代码的人拉出去枪毙;这里记录下线程异常处理的正确方法
父线程能够捕获子线程中抛出的异常么?
答案当然是否定滴
public class ThreadExceptionTest { public static void main(String[] args) { new Thread(() -> { System.out.println(1/0); }).start(); } }
上述代码运行结果如下:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero at com.demo.common.ThreadExceptionTest.lambda$main$0(ThreadExceptionTest.java:7) at java.lang.Thread.run(Thread.java:748)
原因是,JVM会调用dispatchUncaughtException方法分发处理线程执行中未被捕获的异常,dispatchUncaughtException方法源码如下:
未指定UncaughtExceptionHandler的情况下,默认使用ThreadGroup作为未捕获异常处理回调,ThreadGroup默认将异常信息输出到控制台
正常的项目中,肯定不会将控制台作为异常信息的展示,一般有两种方法来捕获并输出异常信息:
1、线程执行方法使用try catch包裹起来,并且捕获的异常最好是Exception,避免出现问题难以定位
2、通过设置UncaughtExceptionHandler来捕获并输出异常信息;setDefaultUncaughtExceptionHandler或者setUncaughtExceptionHandler
线程池中通过execute方法和submit方法提交任务
public class ThreadPoolExecuteTest { public static void main(String[] args) { final ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { System.out.println(1/0); }); } }
上述代码执行结果为:
将execute方法替换成submit方法,执行结果如下:
可以看到,通过submit方法提交任务中的异常被“吞掉”了,而execute方法异常正常输出到控制台,原因如下(ThreadPoolExecutor源码):
1、ThreadPoolExecutor.execute方法,先调用addWorker方法(只记录线程池异常问题,忽略部分源码)
2、addWorker方法,根据提交的任务创建了一个Worker对象,Worker类为Thread类的一个子类,结构如下:
3、如上所示,t.start实际执行的是Worker的run方法,Worker的run方法中最终调用了Threadd的run方法;因此execute方法和普通线程异常原理类似,未指定UncaughtExceptionHandler时,使用ThreadGroup将异常输出到控制台
4、同理,正常项目中execute方法提交任务抛出的异常也不能使用控制台输出,execute方法提交任务捕获异常方法:
(1)try catch,不多说了
(2)创建线程池时,通过ThreadFactory为线程池中的线程设置异常捕获回调方法
submit方法提交的任务异常信息为何无任何提示呢?
1、submit方法先将提交的任务封装成FutureTask,然后再调用execute方法
2、execute方法同上,不同的是task.run方法最终调用的是FutrueTask的run方法,可以看到FutureTask最终将执行任务抛出的异常捕获,并且未抛出
3、submit方法提交任务的异常该如何捕获呢?
(1)从根本上解决问题,即不需要处理返回值得任务尽量用execute提交(PS:我基本上没有使用过submit)
(2)try catch
(3)用Future接收submit的返回值,然后获取异常
周期性调度线程池异常处理
在说明异常之前先说一点周期性线程池的易错点
1、scheduleAtFixedRate是否按照指定的period周期性执行?
答案是否定的,下一次任务的开始时间取决于前一次任务何时结束,如果前一次任务的执行时间超过period,那么下一次任务在前一次任务执行结束后执行,否则严格按照period为周期执行
2、scheduleWithFixedDelay是否按照delay,延迟执行?
答案是否定的,下一次任务的开始时间也似取决于前一次任务何时结束,如果前一次任务的执行时间超过delay,那么下一次任务在前一次任务执行结束后执行,否则严格按照delay为延迟执行
接着说线程池异常问题:
public class ScheduleThreadPoolException { public static void main(String[] args) { final AtomicInteger atomicInteger = new AtomicInteger(0); final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); executorService.scheduleAtFixedRate(() -> { System.out.println("Normal execute.."); if (atomicInteger.incrementAndGet() == 3) { System.out.println(1/0); } }, 0, 3, TimeUnit.SECONDS); } }
执行结果如下:
可以看到,任务在调度三次发生异常后,不再正常调度,这是为啥呢?
1、scheduleAtFixedRate方法先将task封装成ScheduleFutureTask,然后进行调度
2、和前面类似,任务执行最终调度的为ScheduleFutureTask类的run方法,重点关注最后一个if分支的runAndReset方法,如果该方法返回true,则继续调度
3、runAndReset方法中,执行了task,如果task抛出异常,则setException以及返回执行失败
4、如何捕获周期性调度线程池中的异常呢?
(1)try catch
(2)接收返回值Future,然后从中获取异常信息