1. 序言
- 实际工作中,我们需要使用
ScheduledExecutor 创建定时任务,以上报metric、打印信息等ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); service.scheduleAtFixedRate(new Runnable() { @Override public void run() { // do something } }, 0, 2, TimeUnit.SECONDS);
- 有些时候会发现监控数据没了,也就是定时上报metric存在某些问题
- 可能是存储metric的influxdb(时序数据库)存在问题,未能成功写入metric
- 也可能是定时任务存在问题,不再定时上报mtric
- 如果有其他监控能正常显示,或者该监控从某段时间开始没有了数据,那一般都是定时上报任务执行失败,不再定时上报metric导致
- 但当你review代码后,觉得定时上报metric的逻辑是如此简单,以至于你都没有在
Runnable.run() 方法中使用最外层的try-catch 处理异常 - 而且就算由于某些原因出现异常,那也应该只是影响这一次调度,而不是后续不再调度
2. 模拟定时任务某次调度出现异常
-
下面的代码,模拟了定时任务在某次调度出现异常的情况
public static void main(String[] args) throws ExecutionException, InterruptedException { ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); // 定义只有一个元素的数组,在Runnable任务中更新元素的值 // 避免直接定义变量parma,Runnable任务中更新值时,提示错误: // Variable 'xxx' is accessed from within inner class, needs to be final or effectively final int[] param = new int[1]; service.scheduleAtFixedRate(new Runnable() { @Override public void run() { // 更新元素的值,如果值是5的倍数,则主动创建并抛出异常,以模拟某些情况下Runnable任务执行出现异常 param[0]++; if (param[0] % 5 == 0) { logger.error("Unknown exception occurred, cron task execution failed"); throw new RuntimeException("Unknown exception occurred, cron task execution failed"); } // 成功执行,打印信息 logger.info("Cron task is running, parma is {} ...", param[0]); } }, 0, 2, TimeUnit.SECONDS); }
-
执行结果如下,可以发现定时任务在出现异常后,不再被调度。如果这里是上报metric的定时任务,那对应的监控也就自然没数据了
3. 原因分析与验证
3.1 ScheduledExecutorService 将Runnable任务封装为ScheduledFutureTask
- 跟踪定时任务的执行
- 发现Runnable任务会被封装为一个返回值为
Void 类型的ScheduledFutureTask
ScheduledFutureTask 继承了FutureTask,也实现了RunnableScheduledFuture接口
- 调用父类的构造方法,创建一个返回值为
null 的FutureTask,关键是返回值为null 的Callable任务
3.2 ScheduledFutureTask的定时调度
-
ScheduledFutureTask将被
ScheduledThreadPoolExecutor#delayedExecute() 方法执行
-
该方法会将ScheduledFutureTask添加到线程池的任务队列中,等待线程池的调度
-
线程池成功调度该任务,将执行
ScheduledThreadPoolExecutor.ScheduledFutureTask#run() 方法
-
ScheduledThreadPoolExecutor.ScheduledFutureTask.run() 是定时任务运行的关键方法,由于这是一个需要定时调度的任务,将执行FutureTask.runAndReset() 方法
- 关于FutureTask.runAndReset() 方法- 它的本质是执行
Callable.call() 来完成一次任务的执行 - 同时,一旦此次执行出现任何异常,会导致FutureTask异常结束并且返回false,不再进行定时调度
- 它的本质是执行
-
其中,
FutureTask.setException() 方法,使得Future未正常执行结束或未被cancel时,返回一个cause为Throwable t 的ExecutionException
3.3 通过Future.get() 方法,验证定时任务的异常结束
- 如果我们通过
Future.get() 获取执行结果,在某次调度出现异常后,get()方法将异常退出并抛出ExecutionException - 改上面的demo,通过get()方法获取执行结果
public static void main(String[] args) throws ExecutionException, InterruptedException { ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); ... // 已有代码省略,只展示新增代码 // 获取定时任务的结果,如果定时任务正常调度,get()方法将永远不会返回 // 如果定时任务被取消或因为异常终止,get()方法将抛出异常 try { scheduledFuture.get(); } catch (ExecutionException exception) { // logger.error("Exception occurred in cron task", exception.getCause()); } catch (Throwable throwable) { logger.error("Cron task canceled or abnormal terminated", throwable); } }
- 从执行结果可以看出:当定时任务运行失败时,对应的FutureTask确实返回了ExecutionException,且异常的cause就是导致任务失败的RuntimeException
3.4 简单粗暴的处理方法
-
解决办法: 对Runnable任务,使用最外层的
try-catch 捕获并处理异常- 一方面,可以使封装得到的ScheduledFutureTask不会因为Callable.call()抛出异常而停止定时调度
- 另一方面,及时抛出异常、停止调度,也可以打印相关信息,提示定时任务的异常退出
-
在示例代码中,笔者认为某次的失败不应该导致定时任务退出,而是可以继续进行下一次调度。因此笔者只是简单的捕获并打印异常
public static void cronTaskTest() { ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); int[] param = new int[1]; ScheduledFuture<?> scheduledFuture = service.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { // 更新元素的值,如果值是5的倍数,则主动创建并抛出异常,以模拟某些情况下Runnable任务执行出现异常 param[0]++; if (param[0] % 5 == 0) { logger.error("Unknown exception occurred, cron task execution failed"); throw new RuntimeException("Unknown exception occurred, cron task execution failed"); } // 成功执行,打印信息 logger.info("Cron task is running, parma is {} ...", param[0]); } catch (Throwable throwable) { // 捕获任何异常,使得ScheduledFutureTask每次调度时不会抛出异常 logger.error("Timed task scheduling failed",throwable); } } }, 0, 2, TimeUnit.SECONDS); // 获取定时任务的结果,如果定时任务正常调度,get()方法将永远不会返回 —— 这里的get()方法将永远不会返回 // 如果定时任务被取消或因为异常终止,get()方法将抛出异常 try { scheduledFuture.get(); } catch (ExecutionException exception) { logger.error("Exception occurred in cron task", exception.getCause()); } catch (Throwable throwable) { logger.error("Cron task canceled or abnormal terminated", throwable); } }
4. 其他
4.1 Runnable vs Callable
- 对于Runnable和Callable任务,除了能否返回结果之外,最大的区别就是是否支持上抛异常。具体可以参考笔者之前的博客:Runnable vs. Callable in Java
- 同时,Callable接口的源码注释也可以看出二者的差异