ScheduledThreadPoolExecutor踩过最痛的坑

概述最近项目上反馈某个重要的定时任务突然不执行了,很头疼,开发环境和测试环境都没有出现过这个问题 。定时任务采用的是
ScheduledThreadPoolExecutor,后来一看代码发现踩了一个大坑....
还原"大坑"这个坑就是如果
ScheduledThreadPoolExecutor中执行的任务出错抛出异常后,不仅不会打印异常堆栈信息,同时还会取消后面的调度, 直接看例子 。
@Testpublic void testException() throws InterruptedException {// 创建1个线程的调度任务线程池ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();// 创建一个任务Runnable runnable = new Runnable() {volatile int num = 0;@Overridepublic void run() {num ++;// 模拟执行报错if(num > 5) {throw new RuntimeException("执行错误");}log.info("exec num: [{}].....", num);}};// 每隔1秒钟执行一次任务scheduledExecutorService.scheduleAtFixedRate(runnable, 0, 1, TimeUnit.SECONDS);Thread.sleep(10000);}运行结果:

ScheduledThreadPoolExecutor踩过最痛的坑

文章插图
 
  • 只执行了5次后,就不打印,不执行了,因为报错了
  • 任务报错,也没有打印一次堆栈,更导致调度任务取消,后果十分严重 。
解决方案解决方法也非常简单,只要通过try catch捕获异常即可 。
ScheduledThreadPoolExecutor踩过最痛的坑

文章插图
 
运行结果:
ScheduledThreadPoolExecutor踩过最痛的坑

文章插图
 
看到不仅打印了异常堆栈,而且也会进行周期性的调度 。
更推荐的做法更好的建议可以在自己的项目中封装一个包装类,要求所有的调度都提交通过我们统一的包装类,如下代码:
@Slf4jpublic class RunnableWrApper implements Runnable {// 实际要执行的线程任务private Runnable task;// 线程任务被创建出来的时间private long createTime;// 线程任务被线程池运行的开始时间private long startTime;// 线程任务被线程池运行的结束时间private long endTime;// 线程信息private String taskInfo;private boolean showWaitLog;/*** 执行间隔时间多久,打印日志*/private long durMs = 1000L;// 当这个任务被创建出来的时候,就会设置他的创建时间// 但是接下来有可能这个任务提交到线程池后,会进入线程池的队列排队public RunnableWrapper(Runnable task, String taskInfo) {this.task = task;this.taskInfo = taskInfo;this.createTime = System.currentTimeMillis();}public void setShowWaitLog(boolean showWaitLog) {this.showWaitLog = showWaitLog;}public void setDurMs(long durMs) {this.durMs = durMs;}// 当任务在线程池排队的时候,这个run方法是不会被运行的// 但是当任务结束了排队,得到线程池运行机会的时候,这个方法会被调用// 此时就可以设置线程任务的开始运行时间@Overridepublic void run() {this.startTime = System.currentTimeMillis();// 此处可以通过调用监控系统的API,实现监控指标上报// 用线程任务的startTime-createTime,其实就是任务排队时间// 这边打印日志输出,也可以输出到监控系统中if(showWaitLog) {log.info("任务信息: [{}], 任务排队时间: [{}]ms", taskInfo, startTime - createTime);}// 接着可以调用包装的实际任务的run方法try {task.run();} catch (Exception e) {log.error("run task error", e);throw e;}// 任务运行完毕以后,会设置任务运行结束的时间this.endTime = System.currentTimeMillis();// 此处可以通过调用监控系统的API,实现监控指标上报// 用线程任务的endTime - startTime,其实就是任务运行时间// 这边打印任务执行时间,也可以输出到监控系统中if(endTime - startTime > durMs) {log.info("任务信息: [{}], 任务执行时间: [{}]ms", taskInfo, endTime - startTime);}}}使用:
ScheduledThreadPoolExecutor踩过最痛的坑

文章插图
 
我们还可以在包装类里面封装各种监控行为,如本例打印日志执行时间等 。
原理探究那大家有没有想过为什么任务出错会导致异常无法打印,甚至调度都取消了呢?让我们从源码出发,一探究竟 。
  1. 下面是调度任务的入口方法 。
// ScheduledThreadPoolExecutor#scheduleAtFixedRatepublic ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit) {if (command == null || unit == null)throw new NullPointerException();if (period <= 0)throw new IllegalArgumentException();// 将执行任务和参数包装成ScheduledFutureTask对象ScheduledFutureTask<Void> sft =new ScheduledFutureTask<Void>(command,null,triggerTime(initialDelay, unit),unit.toNanos(period));RunnableScheduledFuture<Void> t = decorateTask(command, sft);sft.outerTask = t;// 延迟执行delayedExecute(t);return t;}


推荐阅读