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);
}
相关推荐
鬼火儿4 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin5 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧6 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧6 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧6 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧6 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧6 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧6 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧6 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang7 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构