ScheduledThreadPoolExecutor 定时调度增强:DelayedWorkQueue 二叉堆延时队列与 Spring 体系实战

🚀 道格·李为什么需要一个能定时的线程池

Java 1.3 引入的 java.util.Timer 是 JDK 最早提供的定时任务工具。但它有两个致命设计缺陷:① 单线程执行——一个任务执行时间过长,后面的所有任务都会延迟;② 异常吞没导致线程终止——任务抛了未捕获异常,Timer 线程静默死亡,剩余任务永远不会执行。第二个问题在生产环境尤其危险——线上定时取消超时订单的任务因为一个 NullPointerException 静默停止,几天后才被发现。

道格·李在设计 JSR 166 时,ScheduledThreadPoolExecutorThreadPoolExecutor 的直接扩展。它的设计策略是复用线程池的全部管理能力(线程生命周期、拒绝策略、钩子方法),只替换两个关键组件:

  1. 任务队列:用 DelayedWorkQueue(基于二叉堆的延时队列)替换 BlockingQueue,任务按触发时间排序,堆顶是最先到期的任务
  2. 任务类型:用 ScheduledFutureTask 替换普通的 FutureTask,增加了周期执行模式(固定频率 vs 固定延迟)和下次触发时间的计算逻辑

核心改进:线程池里有 N 个工作线程,一个任务异常不会影响其他线程和任务。Timer 的单线程弱点不再存在。

🏊 ScheduledThreadPoolExecutor 的整体架构

🔗 继承关系与组件概览

ScheduledThreadPoolExecutor 直接继承 ThreadPoolExecutor,在父类基础上替换了三个关键组件:

flowchart LR
    %% ==========================================
    %% 样式定义
    %% ==========================================
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;
classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold;
classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;

    ROOT[ScheduledThreadPoolExecutor\n继承 ThreadPoolExecutor]

    ROOT --> B1(1. 任务类型替换)
    B1 --> TASK["📦 ScheduledFutureTask\nextends FutureTask\n+ implements Delayed\n+ 三态 period 模型\n+ sequenceNumber 保序"]

    ROOT --> B2(2. 队列替换)
    B2 --> QUEUE["📥 DelayedWorkQueue\n自建二叉堆\n无界阻塞延迟队列\n扩展 leader/follower 模式"]

    ROOT --> B3(3. 调度方法替代)
    B3 --> SCHED["⚡ 三个入口方法"]
    SCHED --> S1["schedule()\n一次性延迟任务\nperiod = 0"]
    SCHED --> S2["scheduleAtFixedRate()\n固定速率\nperiod > 0"]
    SCHED --> S3["scheduleWithFixedDelay()\n固定延迟\nperiod < 0"]

    ROOT --> B4(4. 关闭后行为)
    B4 --> SHUT["🛑 两个布尔开关"]
    SHUT --> C1["continueExistingPeriodicTasksAfterShutdown\nshutdown 后是否继续执行周期任务"]
    SHUT --> C2["executeExistingDelayedTasksAfterShutdown\nshutdown 后是否执行已延迟的任务"]

    ROOT --> B5(5. 线程数策略)
    B5 --> SIZE["🔢 maximumPoolSize = Integer.MAX_VALUE\n队列无界,永远不需要额外线程\n仅核心线程数决定并发度"]

    class ROOT root;
    class B1,B2,B3,B4,B5 branch;
    class TASK,QUEUE,SCHED,SHUT,SIZE leaf;
    class S1,S2,S3,C1,C2 highlight;

五个改进点一句话总结

改进维度ThreadPoolExecutorScheduledThreadPoolExecutor
任务类型普通 Runnable / FutureTaskScheduledFutureTask,携带时间戳与周期信息
存储队列任意 BlockingQueue(由用户传入)固定为 DelayedWorkQueue(自建二叉堆)
调度入口execute() 三步决策模型schedule() / scheduleAtFixedRate() / scheduleWithFixedDelay()
线程数模型corePoolSize + maximumPoolSize 灵活伸缩fixed corePoolSize,maximumPoolSize 恒为 Integer.MAX_VALUE
关闭行为shutdown() 后队列不再接受任务通过两个布尔参数控制是否继续执行已有延迟/周期任务

📋 构造函数:队列和线程数被锁定

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
}

逐行解读

  • corePoolSize:由用户指定,决定了并发调度线程数
  • Integer.MAX_VALUE:maximumPoolSize 硬编码为最大值,因为队列是无界的,永远走不到"队列满→创建额外线程"的分支。这不是设计漏洞,而是有意为之DelayedWorkQueue 无界,队列永远不会满,maximumPoolSize 参数实际上失效,直接设为最大值避免误解
  • 0, NANOSECONDS:keepAliveTime 设为 0,配合 NANOSECONDS 时间单位,因为不存在需要回收的超额线程
  • new DelayedWorkQueue():队列固定为用户不可替换的 DelayedWorkQueue

特别注意:用户不能传入自定义队列。ScheduledThreadPoolExecutor 只有一个公开构造函数,队列被固定为 DelayedWorkQueue() 的匿名实例。这意味着所有定时调度能力都建立在这个自建二叉堆之上。

🔮 核心数据结构:ScheduledFutureTask

ScheduledFutureTask 是定时任务的载体。它同时扮演三个角色:

classDiagram
    class Runnable {
        <>
        +run()
    }
    class Future {
        <>
        +cancel() +get()
    }
    class Delayed {
        <>
        +getDelay(TimeUnit)
    }
    class FutureTask {
        -state
        -outcome
    }
    class ScheduledFutureTask {
        -long sequenceNumber
        -long time
        -long period
        -RunnableScheduledFuture outerTask
        -int heapIndex
        +run()
        +cancel()
        +getDelay()
        +compareTo()
    }

    Runnable <|.. FutureTask
    Future <|.. FutureTask
    FutureTask <|-- ScheduledFutureTask
    Delayed <|.. ScheduledFutureTask
    Runnable <|.. ScheduledFutureTask

五个核心字段:

字段类型含义
sequenceNumberlong全局递增序列号,用于相同延迟时间的任务之间的 FIFO 排序
timelong任务下次可执行的纳秒级时间戳(System.nanoTime() + delay
periodlong正值=固定速率、负值=固定延迟、0=一次性任务
outerTaskRunnableScheduledFuture指向自身,用于 reExecutePeriodic 重新入队
heapIndexint在 DelayedWorkQueue 二叉堆中的位置,加速取消操作

🔢 period 的三态模型

periodScheduledFutureTask 最重要的字段,它的三种取值决定了任务的重复策略:

flowchart TD
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;

    P{period 值判断} --> ZERO[period == 0]
    P --> POSITIVE[period > 0]
    P --> NEGATIVE[period < 0]

    ZERO --> Z_DESC["一次性延迟任务\n执行一次后被丢弃\n对应 schedule() 方法"]

    POSITIVE --> P_DESC["固定速率 FixedRate\n两次开始时间间隔固定\n对应 scheduleAtFixedRate()"]
    P_DESC --> P_TIME["下次 time = time + period\n不依赖当前时间,避免累积延迟"]

    NEGATIVE --> N_DESC["固定延迟 FixedDelay\n前次结束→后次开始间隔固定\n对应 scheduleWithFixedDelay()"]
    N_DESC --> N_TIME["下次 time = now + (-period)\n基于当前时间,不受前次执行耗时影响"]

    class P condition;
    class ZERO,POSITIVE,NEGATIVE process;
    class Z_DESC,P_DESC,P_TIME,N_DESC,N_TIME data;

源码验证

// ScheduledFutureTask.java — 计算下次执行时间
private void setNextRunTime() {
    long p = period;
    if (p > 0) // 固定速率:在当前 time 上累加,不依赖系统时间
        time += p;
    else        // 固定延迟(p < 0):基于当前时间重新计算
        time = triggerTime(-p);
}

// 判断是否是周期任务
public boolean isPeriodic() {
    return period != 0;
}

triggerTime 将延迟值转为纳秒时间戳:

private long triggerTime(long delay, TimeUnit unit) {
    return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}

long triggerTime(long delay) {
    // nanoTime() 可能溢出,用 long 的模运算特性自动处理
    return System.nanoTime() +
        ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

overflowFree 处理纳秒时间戳的数值溢出问题——System.nanoTime() 可以正可以负,当它接近 Long.MAX_VALUE 时,直接加一个大的 delay 会导致溢出。overflowFree 通过比较队列头部的时间戳来修正。

比较规则:延迟时间优先,序列号保 FIFO

二叉堆中的排序规则由 compareTo 决定:

public int compareTo(Delayed other) {
    if (other == this)
        return 0;
    if (other instanceof ScheduledFutureTask) {
        ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
        long diff = time - x.time;
        if (diff < 0) return -1;
        else if (diff > 0) return 1;
        // 相同延迟时间时,用 sequenceNumber 保证 FIFO
        else if (sequenceNumber < x.sequenceNumber) return -1;
        else return 1;
    }
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}

关键点:当两个任务的 time 相同时,sequenceNumber 较小的任务排在前面。这保证了即使 1000 个任务设了相同的延迟时间,它们也会按提交顺序 FIFO 执行,而不是随机顺序。

🏗️ 核心数据结构:DelayedWorkQueue

DelayedWorkQueueScheduledThreadPoolExecutor 的发动机。JDK 标准库中已经有一个 DelayQueue(内部复用 PriorityQueue 的二叉堆),但 ScheduledThreadPoolExecutor 选择自建了一个二叉堆。

为什么不直接用 DelayQueue?

对比维度DelayQueueDelayedWorkQueue
堆实现委托 PriorityQueue 的二叉堆自建二叉堆(数组实现)
取消效率O(n) 线性查找O(log n) 堆化,配合 heapIndex 字段快速定位
元素类型泛型 E extends Delayed硬编码 RunnableScheduledFuture,避免了类型擦除带来的额外操作
leader/follower有,减少不必要的唤醒
内存分配PriorityQueue 初始容量 11初始容量 16

根本原因DelayQueueremove() 取消任务需要 O(n) 线性扫描。对于高频定时场景(如每秒千级任务提交和取消),线性扫描是不可接受的。ScheduledFutureTask 持有 heapIndex 字段记录在堆中的位置,删除时直接定位后在局部堆化,降到了 O(log n)。

🏗️ 二叉堆结构

flowchart TD
classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;

    subgraph HEAP["DelayedWorkQueue 最小堆"]
        ROOT["queue[0]: 最小 time\n最早触发的任务"] --> L["queue[1]\ntime ≥ root.time"]
        ROOT --> R["queue[2]\ntime ≥ root.time"]
        L --> LL["queue[3]"]
        L --> LR["queue[4]"]
        R --> RL["queue[5]"]
        R --> RR["queue[6]"]
    end

    NOTE["⚡ 堆序性质:父节点的 time ≤ 子节点的 time\nqueue[0] 永远是最近需要执行的任务\nsiftUp(): 插入时自底向上修复\nsiftDown(): 删除时自顶向下修复"]

    class ROOT highlight;
    class L,R,LL,LR,RL,RR data;
    class NOTE process;

核心字段 (来自源码):

static class DelayedWorkQueue extends AbstractQueue<Runnable>
    implements BlockingQueue<Runnable> {

    private static final int INITIAL_CAPACITY = 16;
    private RunnableScheduledFuture<?>[] queue =
        new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
    private final ReentrantLock lock = new ReentrantLock();
    private int size;
    private Thread leader;
    private final Condition available = lock.newCondition();
}

逐字段解释

  • queue:底层数组,二叉堆的物理存储。索引 0 是堆顶(最小 time 的任务)
  • lock:所有入队/出队操作的互斥锁,保证堆结构的线程安全
  • leader:leader-follower 模式中的 leader 线程,用于减少不必要的线程唤醒
  • available:条件变量,线程在此等待任务到期

📐 leader/follower 模式

当多个线程来取任务但堆顶任务尚未到达执行时间时,标准做法是让所有线程各自 await(timeout) 然后醒来抢任务。这样会引发"惊群效应"(多个线程同时被唤醒,但只有一个能拿到任务,其他线程白白唤醒又等待)。

DelayedWorkQueue 用 leader/follower 模式解决:

sequenceDiagram
    participant T1 as 线程1
    participant T2 as 线程2
    participant Q as DelayedWorkQueue
    participant TASK as ScheduledFutureTask

    T1->>Q: take()
    Q->>Q: 检查堆顶任务
    Q-->>T1: delay > 0, 任务未到期
    Q->>Q: leader 设为 T1
    T1->>T1: awaitNanos(delay) 限时等待

    T2->>Q: take()
    Q->>Q: leader != null (T1 已经在等)
    T2->>T2: await() 无限等待(follower 睡眠)

    Note over T1: delay 到期,被唤醒
    T1->>Q: lock.lock()
    Q->>Q: leader = null
    Q-->>T1: 返回堆顶任务
    T1->>T1: 执行任务
    Q->>Q: signal() 唤醒一个 follower
    T2->>T2: 被唤醒,成为新的 leader
    T2->>Q: awaitNanos(newDelay) 重新等待

源码验证

public RunnableScheduledFuture<?> take() throws InterruptedException {
    lock.lockInterruptibly();
    try {
        for (;;) {
            RunnableScheduledFuture<?> first = queue[0];
            if (first == null) {
                available.await();        // 队列空,无限等待
            } else {
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    return finishPoll(first);  // 已到期,取出并重新堆化
                first = null;
                if (leader != null)
                    available.await();    // 有其他线程在等,无限睡眠(follower)
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay);  // leader 限时等待
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && queue[0] != null)
            available.signal();   // 没有 leader 但堆非空,唤醒一个 follower
        lock.unlock();
    }
}

关键点

  • leader 线程使用 awaitNanos(delay) 精确等待到任务到期时间
  • follower 线程使用 await() 无限期等待,不消耗 CPU
  • leader 取走任务后释放锁之前调用 signal() 唤醒一个 follower,被唤醒的 follower 成为新 leader
  • 这就避免了"两个线程同时等待同一任务"的情况,一次只有一个线程计时等待

📋 任务提交与调度流程

📋 三个调度方法的底层统一入口

// 一次性延迟任务
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    RunnableScheduledFuture<?> t = decorateTask(command,
        new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit)));
    delayedExecute(t);
    return t;
}

// 固定速率任务
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
        long initialDelay, long period, TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command, null,
            triggerTime(initialDelay, unit), unit.toNanos(period));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;  // 指向自己,用于 reExecutePeriodic 重新入队
    delayedExecute(t);
    return t;
}

// 固定延迟任务
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
        long initialDelay, long delay, TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command, null,
            triggerTime(initialDelay, unit), unit.toNanos(-delay));  // 传入负值
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}

三个方法的结构完全一致:① 计算触发时间 → ② 构造 ScheduledFutureTask → ③ 调用 decorateTask(扩展点,默认直接返回)→ ④ delayedExecute(t)

唯一区别在于 period

  • schedule:period = 0
  • scheduleAtFixedRate:period = unit.toNanos(period)(正值)
  • scheduleWithFixedDelay:period = unit.toNanos(-delay)(负值)

▶️ delayedExecute:调度入口

private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
        reject(task);                          // 池已关,拒绝
    else {
        super.getQueue().add(task);            // 入队到 DelayedWorkQueue
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))                     // 二次检查:池关闭且不允许执行,移除
            task.cancel(false);
        else
            ensurePrestart();                  // 确保至少有一个工作线程
    }
}

相比 ThreadPoolExecutor 的 execute()delayedExecute 更简单

对比维度ThreadPoolExecutor.execute()ScheduledThreadPoolExecutor.delayedExecute()
判断核心线程数是,小于 corePoolSize 则 addWorker否,统一先入队
判断队列满是,满则创建非核心线程或拒绝否,队列无界永不満
判断最大线程数否,maximumPoolSize=Integer.MAX_VALUE
兜底检查队列满且线程达到上限 → 拒绝入队后二次检查池关闭状态

为什么 Scheduled 版本不需要三步决策模型? 因为 DelayedWorkQueue 是无界队列,任务一定可以入队;maximumPoolSize 无实际限制。唯一需要判断的是池关闭状态,放在 delayedExecutereExecutePeriodic 两处处理即可。

整个调度流程用一张时序图总结:

sequenceDiagram
    participant CALLER as 调用方
    participant STPE as ScheduledThreadPoolExecutor
    participant SFT as ScheduledFutureTask
    participant DWQ as DelayedWorkQueue
    participant WK as Worker线程

    CALLER->>STPE: scheduleAtFixedRate(task, 5, 10, SECONDS)
    STPE->>STPE: triggerTime(5s) → 计算首次触发时间
    STPE->>SFT: new ScheduledFutureTask(task, time, period=10s)
    STPE->>SFT: sft.outerTask = self
    STPE->>STPE: decorateTask() → 扩展点
    STPE->>STPE: delayedExecute(t)

    STPE->>STPE: isShutdown()? → false
    STPE->>DWQ: add(task) — 入二叉堆
    DWQ->>DWQ: siftUp() 堆化
    STPE->>STPE: ensurePrestart() → 启动Worker

    WK->>DWQ: take() — 阻塞获取任务
    DWQ->>DWQ: leader/follower 等待到期
    DWQ-->>WK: 任务到期,返回 ScheduledFutureTask

    WK->>SFT: run()
    SFT->>SFT: isPeriodic() → true
    SFT->>SFT: runAndReset() → 执行并重置 FutureTask 状态
    SFT->>SFT: setNextRunTime()
    alt period > 0 (FixedRate)
        SFT->>SFT: time += period
    else period < 0 (FixedDelay)
        SFT->>SFT: time = now + (-period)
    end
    SFT->>STPE: reExecutePeriodic(outerTask)
    STPE->>DWQ: add(task) — 重新入队

📊 scheduleAtFixedRate vs scheduleWithFixedDelay 对比

这两个方法的区别是高频面试题,本质差异在于**“间隔"的计时起点不同**:

flowchart TD
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca,font-weight:bold;

    subgraph RATE ["📊 固定速率 (FixedRate)"]
        direction TB
        S1["开始1"] --> E1["结束1"]
        E1 --> G1["等待8s"]
        G1 --> S2["开始2"]
        S2 --> E2["结束2"]
        E2 --> G2["等待7s"]
        G2 --> S3["开始3"]
    end

    subgraph DELAY ["📈 固定延迟 (FixedDelay)"]
        direction TB
        S1B["开始1"] --> E1B["结束1"]
        E1B --> D1["延迟10s"]
        D1 --> S2B["开始2"]
        S2B --> E2B["结束2"]
        E2B --> D2["延迟10s"]
        D2 --> S3B["开始3"]
    end

    NOTE["⚡ 核心差异\n\n🔹 FixedRate:开始间隔固定\n任务耗时压缩在间隔内\n\n🔹 FixedDelay:结束到开始固定\n间隔不受任务耗时影响"]

    class S1,S2,S3,S1B,S2B,S3B startEnd;
    class E1,E2,E1B,E2B,G1,G2,D1,D2 process;
    class NOTE highlight;

选择策略

  • 需要按固定频率采集数据(如每秒统计一次 QPS,不管统计过程耗时多少),用 scheduleAtFixedRate
  • 需要任务之间保持固定间隔(如上一次数据库写入完成后等 5 秒再写下一次),用 scheduleWithFixedDelay

📋 任务执行:run() 与周期重入

ScheduledFutureTask 重写了 FutureTask.run(),这是整个周期调度最核心的改造:

public void run() {
    boolean periodic = isPeriodic();                       // step1: 判断周期
    if (!canRunInCurrentRunState(periodic))                // step2: 池状态检查
        cancel(false);
    else if (!periodic)
        super.run();                                       // step3: 一次性任务,走父类
    else if (super.runAndReset()) {                        // step4: 周期任务
        setNextRunTime();                                  // step5: 计算下次执行时间
        reExecutePeriodic(outerTask);                      // step6: 重新入队
    }
}

逐步骤解析

  1. isPeriodic()return period != 0,非零即周期任务
  2. canRunInCurrentRunState(periodic):检查当前池状态是否允许执行。RUNNING 状态一律放行;SHUTDOWN 状态只放行同时满足 run-after-shutdown 策略的任务;STOP/TIDYING/TERMINATED 一律拒绝
  3. 一次性任务走 FutureTask.run(),执行后 FutureTask 的 state 转为 COMPLETING → NORMAL,任务结束
  4. 周期任务走 runAndReset(),执行 callable.call() 后不设置返回值状态,而是重置状态为 NEW,这样同一个 FutureTask 对象可以反复执行
  5. setNextRunTime():根据 period 正/负决定累加还是重新计算
  6. reExecutePeriodic(outerTask):将自身重新放入队列,等待下一次调度

📊 runAndReset 与 run 的区别

这是 FutureTask 的两个方法:

// run() — 执行后设结果,任务终结
public void run() {
    // ... CAS 设置 runner, 执行 callable.call(), 设置 outcome
    set(result);  // state 变为 COMPLETING → NORMAL
}

// runAndReset() — 执行后重置状态,任务可复用
protected boolean runAndReset() {
    // ... CAS 设置 runner, 执行 callable.call()
    // 不调用 set()
    // state 保持 NEW,下一次调用仍然可以执行
}

周期任务必须是可复用的——同一个 ScheduledFutureTask 对象要被执行几百次、几千次。runAndReset 正是为此设计的。

📊 reExecutePeriodic 与 delayedExecute 的区别

void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    if (canRunInCurrentRunState(true)) {    // 池状态允许
        super.getQueue().add(task);          // 重新入队
        if (!canRunInCurrentRunState(true) && remove(task))
            task.cancel(false);             // 入队后二次检查
        else
            ensurePrestart();               // 确保有线程
    }
}

delayedExecute 的区别:

对比维度delayedExecutereExecutePeriodic
调用时机用户首次提交任务周期任务执行完后
池关闭处理reject(task) 拒绝策略静默丢弃,不触发拒绝
二次检查关键词isShutdown()canRunInCurrentRunState(true)

为什么 reExecutePeriodic 不触发拒绝策略? 因为拒绝策略(如 AbortPolicy 抛异常)发生在一个 Worker 线程的内部执行循环中。如果这里抛出异常,会绕过 afterExecute,甚至可能终止 Worker 线程。静默丢弃是更安全的选择。

从 ThreadPoolExecutor 视角看全部改进

在了解各组件细节后,把这五个改进放在一起做一个全景对比:

flowchart TD
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;

    subgraph TPE["ThreadPoolExecutor 的任务提交流程"]
        direction TB
        T1[用户提交] --> T2{当前线程数\n小于 corePoolSize?}
        T2 -- 是 --> T3[addWorker 创建线程]
        T2 -- 否 --> T4{队列已满?}
        T4 -- 否 --> T5[入队等待]
        T4 -- 是 --> T6{线程数\n小于 maxPoolSize?}
        T6 -- 是 --> T7[addWorker 创建额外线程]
        T6 -- 否 --> T8[执行拒绝策略]
    end

    subgraph STPE["ScheduledThreadPoolExecutor 的调度流程"]
        direction TB
        S1[用户提交 scheduleXXX] --> S2[new ScheduledFutureTask\n计算触发时间 + 设定周期]
        S2 --> S3[delayedExecute]
        S3 --> S4[入 DelayedWorkQueue 二叉堆]
        S4 --> S5[ensurePrestart 确保有 Worker]
        S5 --> S6[Worker 取任务 leader/follower 等待]
        S6 --> S7[任务到期后执行 run]
        S7 --> S8{isPeriodic?}
        S8 -- 是 --> S9["setNextRunTime\n固定速率: time+period\n固定延迟: now+period(绝对值)"]
        S9 --> S10[reExecutePeriodic 重新入队]
        S8 -- 否 --> S11[任务结束]
    end

    class T1,T3,T5,T7,T8,S1,S4,S5,S6,S7,S9,S10,S11 process;
    class T2,T4,T6,S8 condition;
    class S2,S3 highlight;

五处源码级改造汇总:

序号改造点ThreadPoolExecutor 原实现ScheduledThreadPoolExecutor 改造源码位置
任务载体Runnable / FutureTaskScheduledFutureTask,增加 time / period / sequenceNumber / heapIndexScheduledThreadPoolExecutor.ScheduledFutureTask
任务队列用户可替换的任意 BlockingQueue硬编码 DelayedWorkQueue,自建二叉堆 + leader/followerScheduledThreadPoolExecutor.DelayedWorkQueue
任务提交execute(Runnable) 三步决策delayedExecute(RunnableScheduledFuture) 入队 + 预启动ScheduledThreadPoolExecutor.delayedExecute()
任务执行FutureTask.run() 执行后设结果runAndReset() 执行后重置状态,周期任务可复用ScheduledFutureTask.run()
线程数模型corePoolSize/maxPoolSize 两级伸缩maxPoolSize=Integer.MAX_VALUE,仅 corePoolSize 决定并发度ScheduledThreadPoolExecutor 构造函数

🛠️ 日常开发中的常用方法

📊 API 速查表

方法签名用途频率
scheduleschedule(Runnable/Callable, delay, unit)延迟执行一次性任务
scheduleAtFixedRatescheduleAtFixedRate(Runnable, initialDelay, period, unit)固定速率周期执行
scheduleWithFixedDelayscheduleWithFixedDelay(Runnable, initialDelay, delay, unit)固定延迟周期执行
setRemoveOnCancelPolicysetRemoveOnCancelPolicy(boolean)取消任务时是否立即从队列移除
setContinueExistingPeriodicTasksAfterShutdownsetContinueExistingPeriodicTasksAfterShutdown(boolean)shutdown 后是否继续周期任务
setExecuteExistingDelayedTasksAfterShutdownsetExecuteExistingDelayedTasksAfterShutdown(boolean)shutdown 后是否执行延迟任务
getQueuegetQueue()获取 DelayedWorkQueue(谨慎操作)
setCorePoolSizesetCorePoolSize(int)动态调整核心线程数
shutdown / shutdownNow(继承)关闭线程池
awaitTerminationawaitTermination(timeout, unit)等待线程池终止

🌐 典型使用场景

场景 1:定时统计 QPS

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

// 每秒打印一次 QPS
scheduler.scheduleAtFixedRate(() -> {
    long qps = counter.getAndSet(0);
    log.info("Current QPS: {}", qps);
}, 1, 1, TimeUnit.SECONDS);

场景 2:异步任务超时取消

ScheduledExecutorService timeoutScheduler = Executors.newScheduledThreadPool(4);

<T> CompletableFuture<T> withTimeout(Callable<T> task, long timeout, TimeUnit unit) {
    CompletableFuture<T> future = CompletableFuture.supplyAsync(() -> {
        try { return task.call(); }
        catch (Exception e) { throw new RuntimeException(e); }
    });

    ScheduledFuture<?> timeoutTask = timeoutScheduler.schedule(() -> {
        future.completeExceptionally(new TimeoutException("任务超时"));
    }, timeout, unit);

    future.whenComplete((r, e) -> timeoutTask.cancel(false));
    return future;
}

场景 3:批量处理任务时控制间隔

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

// 每批处理 100 条,批次之间间隔 5 秒,避免数据库压力
scheduler.scheduleWithFixedDelay(() -> {
    List<Order> batch = orderDao.fetchPendingOrders(100);
    batch.forEach(this::processOrder);
}, 0, 5, TimeUnit.SECONDS);

📊 Executors 工厂方法对比

工厂方法线程数特性corePoolSize 可调
newScheduledThreadPool(n)核心线程数 n,可随时通过 setCorePoolSize 调整
newSingleThreadScheduledExecutor()固定单线程,不可扩展否(重写 setCorePoolSize 为 no-op)

两者的不等价关系——newScheduledThreadPool(1)newSingleThreadScheduledExecutor() 在功能上类似但前者可扩容而后者不行。如果未来可能增加并发度,优先用前者。

🏗️ Spring 体系中的定时调度

Spring 提供了更高层的定时任务抽象(@ScheduledTaskScheduler),它们底层都依赖 ScheduledThreadPoolExecutorScheduledExecutorService

🏗️ Spring 定时调度层次结构

flowchart TD
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;
classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold;
classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;

    ROOT[Spring Task Scheduling]

    ROOT --> L1(注解驱动层)
    L1 --> AS["@Scheduled 注解\n• fixedRate\n• fixedDelay\n• initialDelay\n• cron"]
    L1 --> AE["@EnableScheduling\n导入 SchedulingConfiguration"]

    ROOT --> L2(抽象层)
    L2 --> TSI["TaskScheduler 接口\n• schedule(Runnable, Trigger)\n• scheduleAtFixedRate()\n• scheduleWithFixedDelay()"]
    L2 --> TSE["TaskExecutor 接口\n线程池执行抽象"]

    ROOT --> L3(实现层)
    L3 --> CTTS["ConcurrentTaskScheduler\n包装 ScheduledExecutorService"]
    L3 --> TSTSE["ThreadPoolTaskScheduler\nSpring 管理的 ScheduledThreadPoolExecutor\n支持 Lifecycle 生命周期"]
    L3 --> PES["PeriodicExecutorScheduler\nSpring Boot 3.x 新增\n基于虚拟线程支持"]

    ROOT --> L4(底层)
    L4 --> STPE["JDK ScheduledThreadPoolExecutor\n或者 ScheduledExecutorService 实例"]

    class ROOT root;
    class L1,L2,L3,L4 branch;
    class AS,AE,TSI,TSE,CTTS,TSTSE,PES,STPE leaf;

使用 @Scheduled 注解

@Configuration
@EnableScheduling
public class SchedulingConfig {

    @Scheduled(fixedRate = 10_000)  // 每 10s 执行一次,固定速率
    public void refreshCache() {
        // 刷新缓存
    }

    @Scheduled(fixedDelay = 5_000, initialDelay = 30_000)  // 启动 30s 后首次执行,之后每次结束间隔 5s
    public void syncToDatabase() {
        // 同步到数据库
    }

    @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨 2 点
    public void dailyReport() {
        // 生成日报
    }
}

@Scheduled 的四个参数对应底层 ScheduledThreadPoolExecutor 的哪个方法:

@Scheduled 参数底层对应period 值
fixedRatescheduleAtFixedRate()> 0
fixedDelayscheduleWithFixedDelay()< 0
initialDelay构造函数中的初始延迟
cron通过 CronTrigger 转换为 nextExecutionTime 后驱动

自定义线程池的 Scheduled 任务

默认情况下 @Scheduled 使用单线程执行所有定时任务。生产环境必须自定义线程池:

@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // 设置核心线程数为 8 的 ScheduledThreadPoolExecutor
        taskRegistrar.setScheduler(
            new ScheduledThreadPoolExecutor(8, r -> {
                Thread t = new Thread(r, "scheduled-worker");
                t.setUncaughtExceptionHandler((thread, ex) ->
                    log.error("定时任务异常: {}", ex.getMessage(), ex));
                return t;
            })
        );
    }
}

关键实践:必须设置 UncaughtExceptionHandler。定时任务中的异常如果不被捕获,会导致后续调度静默终止——这和文章开头 Timer 的问题是同一种风险。

⏰ Spring Boot 中的 ThreadPoolTaskScheduler

@Bean
public ThreadPoolTaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    scheduler.setPoolSize(8);                        // 核心线程数
    scheduler.setThreadNamePrefix("scheduled-");
    scheduler.setAwaitTerminationSeconds(60);         // shutdown 时等待任务完成
    scheduler.setWaitForTasksToCompleteOnShutdown(true);
    scheduler.setErrorHandler(t ->                   // 异常处理器
        log.error("Scheduled task error", t));
    return scheduler;
}

ThreadPoolTaskScheduler 的优势:

  1. 实现 DisposableBean,Spring 容器关闭时自动执行 shutdown()
  2. 通过 setWaitForTasksToCompleteOnShutdown(true) 确保关闭前执行完队列中的任务
  3. 内置 ErrorHandler 而非 UncaughtExceptionHandler,异常处理更直观

⚠️ scheduleAtFixedRate 在 Spring 中的陷阱

一个常见线上故障:@Scheduled(fixedRate = 1000) 标注的任务中调用了第三方超时接口(耗时 > 1s),后续调用被无限积压。

根因:单线程池 + fixedRate 不等待上次完成就触发下一次,导致任务在调用方排队。执行时间线如下:

时间:  0s    1s    2s    3s    4s
调度:  T1开始 T2触发 T3触发 T4触发 ...
实际:  T1---(耗时3s)---T2---(耗时3s)---T3...

第 2 次调用延迟到第 3s 才开始,但调度依然按 1s 间隔触发新任务。如果线程池只有 1 个线程,所有触发的新任务都堆积在队列中。

解决方案

// 方案 1:用 fixedDelay 替代 fixedRate
@Scheduled(fixedDelay = 1_000)

// 方案 2:增加线程数
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    scheduler.setPoolSize(4);  // 4 个线程并发
    return scheduler;
}

// 方案 3:任务内部加超时保护
@Scheduled(fixedRate = 1_000)
public void task() {
    future.get(800, TimeUnit.MILLISECONDS);  // 超时
}

完整总结

⚙️ 核心知识点全景图

flowchart LR
classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold;
classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold;
classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;

    CENTER[ScheduledThreadPoolExecutor\nextends ThreadPoolExecutor]

    CENTER --> DS(数据结构)
    DS --> DS1["ScheduledFutureTask\ntime / period / sequenceNumber\nheapIndex / outerTask"]
    DS --> DS2["DelayedWorkQueue\n自建最小二叉堆\nleader/follower 模式\n初始容量 16"]

    CENTER --> FLOW(调度流程)
    FLOW --> FLOW1["delayedExecute\n入队→状态检查→预启动"]
    FLOW --> FLOW2["run()\n周期判断→canRunInCurrentRunState\n→runAndReset→setNextRunTime\n→reExecutePeriodic 重新入队"]

    CENTER --> IMPROV(改造点)
    IMPROV --> IMP1["① 任务载体\n新增时间/周期能力"]
    IMPROV --> IMP2["② 队列\n自建堆替代 PriorityQueue"]
    IMPROV --> IMP3["③ 调度入口\n简化三步模型为单步"]
    IMPROV --> IMP4["④ 执行复用\nrunAndReset 替代 run"]
    IMPROV --> IMP5["⑤ 线程模型\n固定 corePoolSize"]

    CENTER --> SPRING(Spring 整合)
    SPRING --> SP1["@Scheduled\nfixedRate / fixedDelay / cron"]
    SPRING --> SP2["ThreadPoolTaskScheduler\nSpring 管理的生命周期"]
    SPRING --> SP3["SchedulingConfigurer\n自定义线程池配置"]

    class CENTER root;
    class DS,FLOW,IMPROV,SPRING branch;
    class DS1,DS2,FLOW1,FLOW2,SP1,SP2,SP3 leaf;
    class IMP1,IMP2,IMP3,IMP4,IMP5 highlight;

📊 对比总结表

维度ThreadPoolExecutorScheduledThreadPoolExecutor改造动机
任务类型无时间感知的 Runnable带时间戳+周期信息的 ScheduledFutureTask需要知道"何时执行”、“是否重复”
队列通用 BlockingQueue,用户可替换硬编码 DelayedWorkQueue 二叉堆需要按到期时间排序,支持 O(log n) 删除
提交模型execute() 三步决策(核心→队列→最大)delayedExecute() 入队+预启动无界队列 + 无限 maxPoolSize,无需三步
线程伸缩core → queue → max 三级corePoolSize 单级队列永不満,无需额外线程
任务复用一次执行,FutureTask state 终结runAndReset 重置 state,周期复用周期任务需要同一个 FutureTask 对象反复执行
关闭语义shutdown() 后队列任务继续执行两个布尔开关控制是否继续周期/延迟任务关闭后可能仍需消耗已入队的周期任务
maximumPoolSize用户指定,影响线程池伸缩Integer.MAX_VALUE,硬编码避免用户误解为有效参数

三条实践准则

  1. 固定速率用 scheduleAtFixedRate:适合"按固定频率做某事"(日志采样、指标上报),多次调度的起点对齐。但如果任务执行耗时超过间隔时间,下一次调度会立即执行(不会积压),实际上变成连续执行。

  2. 固定延迟用 scheduleWithFixedDelay:适合"做完一件事后等一段时间再做"(批量处理、降级重试),保证任务之间有充足的休息时间,不受单次执行耗时影响。

  3. 生产环境必须自定义线程池@Scheduled 默认单线程,一个任务阻塞会影响所有定时任务。用 ThreadPoolTaskScheduler 或直接构造 ScheduledThreadPoolExecutor,设置线程数 ≥ 2,并注册 ErrorHandler 防止静默失败。