大家好,我是 方圆 。在使用 Java 的 ScheduledThreadPoolExecutor
进行定时任务调度时,默认以为任务在执行时即使抛出异常也不会影响后续任务的执行,如下所示
java
public static void main(String[] args) {
// 创建一个定时任务调度器
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 定时任务,每秒执行一次
scheduler.scheduleAtFixedRate(() -> {
// 业务逻辑
processBiz("执行任务");
}, 0, 1, TimeUnit.SECONDS);
}
然而,在实际开发中却发现:当定时任务中抛出异常后,后续的定时任务不再执行,让人觉得困惑并觉得它好像违背了线程池间任务相互隔离的原则,所以便以本篇文章进行记录和分析。
其实导致这种现象的原因非常简单,主要是因为 ScheduledFutureTask
在执行任务时,如果任务抛出异常,会将任务状态变为 EXCEPTIONAL
,从而阻止了后续的任务调度。下面我们将通过源码分析来深入理解这个问题,以如下代码为例:
java
public class TestScheduledThreadPoolExecutor {
public static void main(String[] args) throws Exception {
System.out.println("=== 深入源码分析:异常如何影响定时任务 ===\n");
analyzeInternalState();
}
/**
* 分析内部状态变化
*/
private static void analyzeInternalState() throws Exception {
System.out.println("通过反射观察 ScheduledFutureTask 内部状态变化:\n");
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
// 创建一个会在第二次执行时抛出异常的任务
final int[] executionCount = {0};
Runnable task = () -> {
executionCount[0]++;
System.out.println("第 " + executionCount[0] + " 次执行");
if (executionCount[0] == 2) {
System.out.println("💥 抛出异常!");
throw new RuntimeException("第二次执行时的异常");
}
System.out.println("任务正常完成");
};
// 调度任务
ScheduledFuture<?> future = executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
// 监控任务状态
monitorTaskState(future);
// 等待足够长的时间观察结果
Thread.sleep(8000);
executor.shutdown();
}
/**
* 监控任务状态
*/
private static void monitorTaskState(ScheduledFuture<?> future) {
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
Thread.sleep(800);
// 通过反射获取内部状态
Field stateField = future.getClass().getSuperclass().getDeclaredField("state");
stateField.setAccessible(true);
int state = (Integer) stateField.get(future);
String stateStr = getStateString(state);
System.out.println("时间点 " + i + ": 任务状态 = " + stateStr + " (" + state + ")");
}
} catch (Exception e) {
System.out.println("监控异常: " + e.getMessage());
}
}).start();
}
/**
* 将状态码转换为可读字符串
*/
private static String getStateString(int state) {
switch (state) {
case 0: return "NEW";
case 1: return "COMPLETING";
case 2: return "NORMAL";
case 3: return "EXCEPTIONAL";
case 4: return "CANCELLED";
case 5: return "INTERRUPTING";
case 6: return "INTERRUPTED";
default: return "UNKNOWN(" + state + ")";
}
}
}
在上述例子中,我们创建了一个定时任务,该任务在第一次执行时正常完成,但在第二次执行时抛出异常,能够发现它在抛出异常后便不再执行任务了,任务状态也由 NEW
变为 EXCEPTIONAL
,如下控制台日志所示:
text
=== 深入源码分析:异常如何影响定时任务 ===
通过反射观察 ScheduledFutureTask 内部状态变化:
时间点 0: 任务状态 = NEW (0)
时间点 1: 任务状态 = NEW (0)
第 1 次执行
任务正常完成
时间点 2: 任务状态 = NEW (0)
第 2 次执行
时间点 3: 任务状态 = NEW (0)
💥 抛出异常!
时间点 4: 任务状态 = NEW (0)
时间点 5: 任务状态 = EXCEPTIONAL (3)
时间点 6: 任务状态 = EXCEPTIONAL (3)
时间点 7: 任务状态 = EXCEPTIONAL (3)
时间点 8: 任务状态 = EXCEPTIONAL (3)
时间点 9: 任务状态 = EXCEPTIONAL (3)
下面我们将通过源码分析来深入理解这个问题。
源码分析
定时任务调度的入口是 scheduleAtFixedRate()
方法,它会创建一个 ScheduledFutureTask
实例并提交到线程池队列中执行。以下是关键代码片段:
java
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
// 创建 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;
}
}
定时任务执行核心的核心逻辑是 ScheduledFutureTask.run()
方法,它会判断任务是否为周期性任务,并根据当前运行状态决定是否执行任务。以下是 ScheduledFutureTask
的关键实现:
java
private class ScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> {
public void run() {
// 判断是否为周期性任务
boolean periodic = isPeriodic();
// 检查当前运行状态是否允许执行
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 如果为一次性任务,则直接调用父类 run 方法执行
else if (!periodic)
ScheduledFutureTask.super.run();
// 执行周期性任务,runAndReset 为关键方法
else if (ScheduledFutureTask.super.runAndReset()) {
// 计算下次执行时间
setNextRunTime();
// 重新调度
reExecutePeriodic(outerTask);
}
}
public boolean isPeriodic() {
// period != 0 表示周期性任务
// period > 0: 固定频率执行(scheduleAtFixedRate)
// period < 0: 固定延迟执行(scheduleWithFixedDelay)
// period == 0: 一次性任务
return period != 0;
}
}
其中 FutureTask.runAndReset()
方法是执行任务的核心逻辑,它会检查任务状态并执行用户定义的 Callable
任务。以下是 runAndReset()
的实现:
java
public class FutureTask<V> implements RunnableFuture<V> {
protected boolean runAndReset() {
// 检查当前状态是否为 NEW
if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
return false;
boolean ran = false;
int s = state;
try {
Callable<V> c = callable;
// 如果 callable 不为 null 且状态为 NEW,则执行任务
if (c != null && s == NEW) {
try {
// 执行用户任务
c.call();
// 标记执行成功
ran = true;
} catch (Throwable ex) {
// 如果任务执行过程中抛出异常,则变更为异常状态
setException(ex);
}
}
} finally {
runner = null;
s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
// 只有成功执行且状态仍为NEW才返回true
return ran && s == NEW;
}
protected void setException(Throwable t) {
// CAS 操作将状态从 NEW 变为 COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
// 将状态设置为 EXCEPTIONAL
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
finishCompletion();
}
}
}
如果在 runAndReset()
中任务执行过程中抛出异常,setException()
会被调用,将任务状态从 NEW
变为 EXCEPTIONAL
。这会导致后续的 runAndReset()
调用返回 false
,导致不会重新调度任务。
否则,如果任务执行成功,状态仍为 NEW
,runAndReset()
方法会返回 true
,可以继续调度,可以继续调度的含义是调用 setNextRunTime()
和 reExecutePeriodic()
方法,重新计算下次执行时间并将任务重新放入调度队列:
java
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {
// 重新执行周期性任务
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) {
// 重新将任务添加到队列中
super.getQueue().add(task);
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
}
总得来说,在任务正常执行的情况下:
- 任务执行成功 → ran = true, state = NEW
runAndReset()
返回 true- 调用
setNextRunTime()
和reExecutePeriodic()
- 任务被重新调度
如果抛出异常:
- 任务抛出异常 →
setException()
被调用 - state 从
NEW
变为EXCEPTIONAL
runAndReset()
返回 false (因为 s != NEW)- 不会调用
setNextRunTime()
和reExecutePeriodic()
- 任务不会被重新调度,定时任务停止
最佳实践
在使用 ScheduledThreadPoolExecutor
时,为了避免任务异常导致后续任务不再执行,需要用户在任务中进行异常处理。这样可以确保即使某个任务抛出异常,也不会影响到其他定时任务的执行:
java
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
try {
// 业务逻辑
processBiz("执行任务");
} catch (Exception e) {
// 捕获异常,防止影响后续任务
logger.error("任务执行异常", e);
}
}, 0, 1, TimeUnit.SECONDS);
}