ScheduledFutureTask 踩坑实录

大家好,我是 方圆 。在使用 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,导致不会重新调度任务。

否则,如果任务执行成功,状态仍为 NEWrunAndReset() 方法会返回 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);
}
相关推荐
掘金码甲哥12 分钟前
Golang 文本模板,你指定没用过!
后端
lwb_011842 分钟前
【springcloud】快速搭建一套分布式服务springcloudalibaba(四)
后端·spring·spring cloud
张先shen2 小时前
Spring Boot集成Redis:从配置到实战的完整指南
spring boot·redis·后端
Dolphin_海豚2 小时前
一文理清 node.js 模块查找策略
javascript·后端·前端工程化
EyeDropLyq3 小时前
线上事故处理记录
后端·架构
MarkGosling6 小时前
【开源项目】网络诊断告别命令行!NetSonar:开源多协议网络诊断利器
运维·后端·自动化运维
Codebee6 小时前
OneCode3.0 VFS分布式文件管理API速查手册
后端·架构·开源
_新一6 小时前
Go 调度器(二):一个线程的执行流程
后端
estarlee6 小时前
腾讯云轻量服务器创建镜像免费API接口教程
后端
风流 少年6 小时前
Cursor创建Spring Boot项目
java·spring boot·后端