之前在编写基于 ScheduledExecutorService (Java Platform SE 8 ) 的定时任务处理逻辑时,发现若任务出现异常,将不会被再调度执行,其文档中也有如下说明:
If any execution of the task encounters an exception, subsequent executions are suppressed.
比如如下代码:
1 | package me.tianshuang; |
控制台将以一秒的时间间隔持续打印 I'm running...
,部分输出如下:
1 | I'm running... |
现在我们在任务中加入异常抛出逻辑,代码如下:
1 | package me.tianshuang; |
控制台打印一次 I'm running...
后就不再打印,输出如下:
1 | I'm running... |
如果拉取栈帧,你会看到线程依然还在,只是处于 WAITING
状态,定时任务的线程对应的栈帧如下:
1 | "pool-1-thread-1@729" prio=5 tid=0xd nid=NA waiting |
而为什么异常后就不再被调度执行了呢,跟踪源码可以发现,在 ScheduledFutureTask
的 run()
方法中会触发对任务的实际调用,源码位于 ScheduledThreadPoolExecutor.java at jdk8-b120:
1 | /** |
其中 runAndReset()
方法实现位于 FutureTask.java at jdk8-b120:
1 | /** |
可以看出,在调用 c.call()
时,外层对 Throwable
进行了 catch 操作,然后调用了 setException
方法,该任务就算执行完成了,而因为 c.call()
调用发生了异常,导致 ran = true;
未被执行,使 runAndReset()
方法的返回值为 false
,从而使 run()
方法的最后一个 else if
块中的代码不会被执行,即该定时任务不会再入队了,这就导致了我们文首提到的问题。
关于该问题,我联想到 Spring 中的定时任务,在 Spring 中的定时任务中,任务异常后依然会被调度执行,这是怎样实现的呢,其实很简单,我们先看 Spring 中的 org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler#scheduleAtFixedRate(java.lang.Runnable, long) 方法:
1 |
|
由以上代码可知,在将任务提交给底层的 ScheduledExecutorService
前,调用了 errorHandlingTask(task, true)
对任务进行了处理,我们看下此方法实现:
1 | private Runnable errorHandlingTask(Runnable task, boolean isRepeatingTask) { |
即使用了 TaskUtils.decorateTaskWithErrorHandler
方法,将需要提交的 task
进行装饰,装饰上 ErrorHandler
以实现错误处理,该接口的源码如下:
1 | /** |
即用于处理异常,我们再看刚提到的 TaskUtils.decorateTaskWithErrorHandler
方法实现:
1 | /** |
根据注释及源码我们知道,该方法用于装饰 task
以处理运行中的错误,最常用的两个 ErrorHandler
实现如下:
1 | /** |
即一种为仅打印日志,不将异常抛出给调用方,一种为打印日志并抛出异常给调用方。确认了 ErrorHandler
的实现后,使用 task
及 ErrorHandler
去创建 Runnable
的包装器 DelegatingErrorHandlingRunnable
以实现装饰,代码如下:
1 | /** |
默认情况下的实现使用的 org.springframework.scheduling.support.TaskUtils.LoggingErrorHandler
,这也是 Spring 中定时任务异常后依然会被触发执行的原因。
Reference
ScheduledExecutorService Exception handling - Stack Overflow