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);
}
相关推荐
bobz96531 分钟前
windows 配置 conda 环境变量
后端
回家路上绕了弯1 小时前
线程池优化实战:从性能瓶颈到极致性能的演进之路
java·后端
bobz9651 小时前
pycharm pro 安装插件失败
后端
丘山子2 小时前
如何规避 A/B Testing 中的致命错误?何时进行 A/B 测试?
前端·后端·面试
用户84913717547163 小时前
JDK 17 实战系列(第4期):安全性与稳定性增强详解
java·后端·性能优化
苏三的开发日记3 小时前
centos如何使用高版本gcc
后端
自由的疯3 小时前
java程序员怎么从Python小白变成Python大拿?(三)
java·后端·trae
用户84913717547163 小时前
JustAuth实战系列(第4期):模板方法模式实战 - AuthDefaultRequest源码剖析
java·后端·架构
lynnss_ai3 小时前
Go + GORM 实现支持嵌套事务的中间件(含事务计数器与日志开关)
后端
_風箏3 小时前
OpenSSH【安装 02】离线升级异常问题解决、无法升级时的失败恢复
后端