Java定时任务不再执行的原因与解决办法

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 tExecutionException
    在这里插入图片描述

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接口的源码注释也可以看出二者的差异
    在这里插入图片描述