调度线程池执行器 ScheduledThreadPoolExecutor

前文三篇文章从 ThreadPoolExecutor 七大核心参数,到 Worker 线程 execute 提交 Runnable 任务,再到 FutureTask 阻塞获取任务结果,以及线程池的两阶段终止,已经对 ThreadPoolExecutor 的基础模型、任务提交、任务封装、线程运行与优雅关闭做了完整拆解。

理解了线程池的运行骨架之后,今天再看看 ThreadPoolExecutor 的 子类 ScheduledThreadPoolExecutor。

通过 UML 类图可以知道,ScheduledThreadPoolExecutor 是在 ThreadPoolExecutor 的基础上增加了时间调度能力的子类扩展。

⨳ 延迟执行一次

方法 说明
schedule(Runnable command, long delay, TimeUnit unit) 延迟指定时间后执行一次 Runnable(无返回值)
schedule(Callable<V> callable, long delay, TimeUnit unit) 延迟指定时间后执行一次 Callable(有返回值 Future)

⨳ 周期性重复执行

方法 说明
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 固定频率执行:每次起始时间 + period 触发,不管上次是否执行完
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 固定延迟执行:每次执行完成后 + delay 才触发下一次

基本使用

创建线程池

不推荐直接使用 Executors.newScheduledThreadPool(),生产建议手动 new,方便自定义线程工厂、拒绝策略。

java 复制代码
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
        2,
        new ThreadFactory() {
            private final AtomicInteger counter = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "scheduled-pool-" + counter.getAndIncrement());
                t.setDaemon(true);
                return t;
            }
        }
);

提交任务

⨳ 单次延迟执行

java 复制代码
executor.schedule(() -> System.out.println("延迟3秒执行"), 3, TimeUnit.SECONDS);

延迟 3 秒后执行一次,执行完任务就结束了。

⨳ 固定速率执行

java 复制代码
executor.scheduleAtFixedRate(() -> {
    try {
        System.out.println("固定速率执行:" + System.currentTimeMillis());
    } catch (Exception e) {
        // 周期性任务必须捕获异常,否则后续调度会被取消
        e.printStackTrace();
    }
}, 1, 5, TimeUnit.SECONDS);

首次延迟 1 秒执行,之后每隔 5 秒开始下一次。如果某次任务执行时间超过 5 秒,下一次会在上一次结束后立即开始,不会并发执行。

⨳ 固定延迟执行

java 复制代码
executor.scheduleWithFixedDelay(() -> {
    try {
        System.out.println("固定延迟执行:" + System.currentTimeMillis());
    } catch (Exception e) {
        e.printStackTrace();
    }
}, 1, 3, TimeUnit.SECONDS);

首次延迟 1 秒执行,之后每次任务执行完,再等 3 秒才执行下一次。

实际间隔 = 任务执行时间 + 3 秒。

scheduleAtFixedRate 和 scheduleWithFixedDelay 会返回 ScheduledFuture<?>,可以用它取消任务。逻辑同 FutureTask。

普通线程池任务丢进阻塞队列,Worker 循环 take 任务,那 ScheduledThreadPoolExecutor 的 Worker 从阻塞队列取任务的时候,是怎么判断任务是否到期?ScheduledFutureTask 又是如何维护周期,是在任务执行完成后重新放回队列吗?

下面深入源码拆解一下。

源码分析

java 复制代码
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

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

ScheduledThreadPoolExecutor 调用父类 ThreadPoolExecutor 的构造器,固定死了最大线程、空闲时间、队列,只开放 corePoolSize、线程工厂、拒绝策略给外部传入。

需要注意的是,STPR 使用的阻塞队列是 DelayedWorkQueue。

DelayedWorkQueue 延期工作队列

前文讲过 延期队列 DelayQueue,DelayQueue 内部持有一个 PriorityQueue 优先级比较队列,可以对剩余延迟时间排序。

java 复制代码
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();

但从继承关系上来看,STPR 的静态内部类 DelayedWorkQueue 并没有复用 DelayQueue,而是重新实现了一套阻塞优先队列。

从成员变量可知,DelayedWorkQueue 和 PriorityQueue 一样,用数组做堆,堆顶永远是最先到期 的任务。只是 DelayQueue 是通用容器,泛型支持任意 Delayed 对象,而 DelayedWorkQueue 只服务 ScheduledFutureTask。

ScheduledFutureTask 调度Future任务

java 复制代码
private class ScheduledFutureTask<V>
		extends FutureTask<V> implements RunnableScheduledFuture<V> {
  // 当**两个任务的 `time`(执行时间)完全相等**时,依靠这个序列号做第二维度排序:序列号小的任务优先执行        
	private final long sequenceNumber;
  //**任务下一次的绝对执行时间(单位:纳秒,基于 `System.nanoTime()`)**
	private volatile long time;
  // 周期标记,区分一次性任务 /fixedRate/fixedDelay
	private final long period;
  // 外部任务引用,默认指向自身
	RunnableScheduledFuture<V> outerTask = this;
  // 记录当前任务在 `DelayedWorkQueue` 堆数组里的下标
	int heapIndex;

ScheduledFutureTask 也是 STPR 的内部私有类,每一个提交给调度线程池的任务都会被封装成这个对象。

从继承关系上,它继承 FutureTask,天然拥有任务状态、结果保存、取消能力;同时实现 RunnableScheduledFuture,兼具 Runnable、Future、Delayed 能力,既能被 Worker 执行,又能在延迟队列里按时间排序。

下面就看看 ScheduledThreadPoolExecutor 是怎么执行延迟任务的。

schedule 延迟执行

schedule() 用于提交延迟执行、只运行一次的 Callable 任务。

java 复制代码
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                       long delay,
                                       TimeUnit unit) {
    if (callable == null || unit == null)
        throw new NullPointerException();
    RunnableScheduledFuture<V> t = decorateTask(callable,
        new ScheduledFutureTask<V>(callable,
                                   triggerTime(delay, unit),
                                   sequencer.getAndIncrement()));
    delayedExecute(t);
    return t;
}

整个方法流程非常清晰:参数校验,封装成 ScheduledFutureTask ,装饰任务,delayedExecute 入队调度,最后返回任务对象。

java 复制代码
ScheduledFutureTask(Callable<V> callable, long triggerTime,
                    long sequenceNumber) {
    super(callable);
    this.time = triggerTime;
    // period 默认等于 0,代表这是一次性任务。
    this.period = 0;
    this.sequenceNumber = sequenceNumber;
}

这是一次性延迟任务对应的构造器 ,调用父类 FutureTask 的构造方法,把传入的业务 Callable 保存起来。

triggerTime 是前面 triggerTime(delay, unit) 算出来的绝对到期时间 。``

java 复制代码
time = System.nanoTime() + unit.toNanos(delay)

这个延迟时间放到任务成员变量的 time 里,堆排序就是比较这个时间。

java 复制代码
private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
        reject(task);
    else {
        super.getQueue().add(task);
        if (!canRunInCurrentRunState(task) && remove(task))
            task.cancel(false);
        else
            ensurePrestart();
    }
}

和普通线程池 execute() 的区别就是,普通线程池是"先判断核心线程",不够先创建 Worker,而 delayedExecute 逻辑非常干脆:先把任务丢进延迟队列,再做状态校验、按需启动 Worker。

java 复制代码
void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}

Worker 线程执行,Worker.run() 循环,阻塞在队列 take() 任务,等待 delay 时间,任务到期,队列返回 task,然后执行 ScheduledFutureTask.run()。

scheduleWithFixedDelay 周期执行

java 复制代码
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0L)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      -unit.toNanos(delay),
                                      sequencer.getAndIncrement());
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}

这是固定延迟调度 的入口方法,首次延迟 initialDelay 之后执行第一次;上一轮任务执行完成,再等待 delay,才开启下一轮。

和延迟执行一样,triggerTime(initialDelay, unit)是第一次任务到期绝对时间(nanoTime)。

period 不一样,对于固定延迟调度来说,这个是个负数,相应的,可以想象到。对于 scheduleAtFixedRate() 固定速率来说,period 应该是个正数。

同理,周期性任务也会放入到 DelayedWorkQueue 队列中,等待 Worker 获取与执行。

无论怎么说,ScheduledFutureTask 重写的 FutureTask.run() 才是调度逻辑的核心。

ScheduledFutureTask.run() 任务执行

java 复制代码
public void run() {
    // 1. 检查线程池当前状态是否允许执行该任务。
    // 如果线程池已经处于关闭(SHUTDOWN)或停止(STOP)状态,则直接取消该任务。
    if (!canRunInCurrentRunState(this)) {
        cancel(false);
    } 
    // 2. 判断该任务是否为非周期性任务(即只执行一次的延迟任务)。
    else if (!isPeriodic()) {
        // 调用父类 FutureTask 的 run 方法,执行具体的 Callable/Runnable 逻辑,并将结果设置到 Future 中。
        // 执行完毕后,该任务的生命周期即结束。
        super.run();
    } 
    // 3. 处理周期性任务(即通过 scheduleAtFixedRate 或 scheduleWithFixedDelay 提交的任务)。
    else if (super.runAndReset()) {
        // runAndReset() 会执行任务并重置状态,但不会设置结果(因为周期性任务没有最终返回值)。
        // 如果任务执行成功(未被中途取消且未抛出异常),则继续以下两步:
        
        // a. 计算并设置该任务下一次应该被执行的时间。
        setNextRunTime();
        
        // b. 将该任务重新放入线程池的延迟队列中,等待下一次触发执行。
        reExecutePeriodic(outerTask);
    }
}

Worker 拿到到期任务后调用这个方法,一次性任务、周期性任务的分支逻辑全部在这里分流。

非周期任务就不看了,和普通线程池一样,正常完成则保存结果;抛出异常则保存异常,执行完就结束,不会再走后面重新入队的逻辑。

重点看一下周期性任务分支 runAndReset()。

java 复制代码
protected boolean runAndReset() {
    // 1. 前置状态检查与 CAS 抢锁
    // 如果当前状态不是 NEW(初始态),或者 CAS 设置当前执行线程(runner)失败,说明任务已被取消、已完成或已有其他线程在执行,直接返回 false。
    if (state != NEW ||
        !RUNNER.compareAndSet(this, null, Thread.currentThread()))
        return false;

    boolean ran = false; // 标记任务是否成功执行过
    int s = state;       // 缓存当前状态
    try {
        Callable<V> c = callable;
        // 再次双重检查:确保 callable 不为空,且状态依然为 NEW(防止在执行前被取消)
        if (c != null && s == NEW) {
            try {
                c.call(); // 核心:执行具体的任务逻辑。注意,这里故意不获取返回值,也不调用 set() 设置结果
                ran = true; // 正常执行完毕,标记为 true
            } catch (Throwable ex) {
                // 如果执行过程中抛出异常,将其封装并设置到 Future 中(后续 get() 时会抛出 ExecutionException)
                setException(ex);
            }
        }
    } finally {
        // 2. 清理执行线程标记
        // 在状态最终确定前,runner 必须保持非空,以防止并发调用 run()
        runner = null; 

        // 3. 处理中断信号
        // 在清空 runner 后必须重新读取 state,以防止出现中断信号泄露(leaked interrupts)
        s = state;
        if (s >= INTERRUPTING)
            // 如果在此期间任务被要求中断,进行相应的中断处理(如自旋等待中断状态设置完成)
            handlePossibleCancellationInterrupt(s);
    }
    // 4. 返回最终结果
    // 只有当任务正常执行完毕(ran == true)且状态依然保持为 NEW(未被取消或异常终止)时,才返回 true。
    // 返回 true 意味着任务"成功执行且已重置",调用者(如 ScheduledFutureTask)可以继续安排下一次执行。
    return ran && s == NEW;
}

因为周期性任务会反复执行,没有所谓的"最终返回值",所以

而且任务正常执行完毕后,不会把任务状态标记为 COMPLETED ,而是重置任务状态,回到初始未运行状态,方便下一轮复用。

下一轮复用的时间由 setNextRunTime() 计算得出。

java 复制代码
private void setNextRunTime() {
    // 获取任务的周期时间(period)。
    // 在 ScheduledThreadPoolExecutor 中,period 的正负号具有特殊含义:
    // 正数代表 scheduleAtFixedRate,负数代表 scheduleWithFixedDelay。
    long p = period;

    if (p > 0) {
        // 【固定频率模式 scheduleAtFixedRate】
        // 下一次执行时间 = 上一次计划的执行时间 + 周期时间。
        // 这种模式下,任务的触发时间是绝对固定的,不受上一次任务执行耗时的影响。
        // 如果任务执行时间超过了周期 p,下一次任务会立即执行(追赶执行)。
        time += p;
    } else {
        // 【固定延迟模式 scheduleWithFixedDelay】
        // 下一次执行时间 = 当前系统时间 + 延迟时间(-p 将负数转回正数)。
        // 这种模式下,每次任务的触发时间都是基于"上一次任务实际执行完毕的时间点"开始计算的。
        // 无论任务执行多久,两次任务的开始时间之间至少会间隔 abs(p) 的时间。
        time = triggerTime(-p);
    }
}

计算出下次执行时间后,最后一步就是把这个任务重新扔回延迟队列里。

需要注意的是,一旦业务代码抛异常,runAndReset() 返回 false,后面 setNextRunTime()、reExecutePeriodic 不会执行,周期性调度就此终止。

所以对于周期性任务来说,需要在任务内部 try-catch 捕获所有异常。

源码鉴赏

ScheduledThreadPoolExecutor 还是挺常用的,比如 SpringBoot 的 @Scheduled 注解,底层就是这个调度线程池执行器。

java 复制代码
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class) 
@Reflective
public @interface Scheduled {
 
    // 禁用 Cron 表达式的特殊标记常量
    String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;
 
    // 【Cron 表达式】
    // 用于定义复杂的定时规则(如"每天上午9点执行"、"每周一执行"等)。
    // 如果指定了 cron,则 fixedRate 和 fixedDelay 必须保持默认值。
    String cron() default "";

    // 【时区】
    // 解析 cron 表达式时使用的时区。如果不指定,默认使用服务器的本地时区。
    String zone() default "";

    // 【固定延迟执行(毫秒)】
    // 上一次任务执行完毕后,等待指定的毫秒数再执行下一次。
    // 对应 JDK 的 scheduleWithFixedDelay。默认值 -1 表示未启用。
    long fixedDelay() default -1;
    
    // 【固定延迟执行(字符串形式)】
    // 作用同 fixedDelay,但支持占位符(如 "${my.delay}")或 SpEL 表达式,方便从配置文件中读取。
    String fixedDelayString() default "";

    // 【固定频率执行(毫秒)】
    // 按照固定的频率间隔执行,以上一次任务的开始时间为基准。
    // 对应 JDK 的 scheduleAtFixedRate。默认值 -1 表示未启用。
    long fixedRate() default -1;
    
    // 【固定频率执行(字符串形式)】
    // 作用同 fixedRate,支持占位符或 SpEL 表达式。
    String fixedRateString() default "";

    // 【初始延迟(毫秒)】
    // 应用启动后,第一次执行任务前需要等待的毫秒数。默认值 -1 表示无延迟,立即执行第一次。
    long initialDelay() default -1;
    
    // 【初始延迟(字符串形式)】
    // 作用同 initialDelay,支持占位符或 SpEL 表达式。
    String initialDelayString() default "";

    // 【时间单位】
    // 指定 fixedDelay、fixedRate 和 initialDelay 的时间单位。默认为毫秒(MILLISECONDS)。
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

    // 【指定调度器】
    // 当配置了多个 TaskScheduler 时,可以通过该属性指定使用哪一个调度器的 Bean 名称。
    String scheduler() default "";
}

是不是注解的属性很熟悉,initialDelay 对应 首次延迟, fixedRate 对应固定速率,fixedDelay 对应 固定延迟。

除此之外,@Scheduled 注解还支持 cron 表达式,cron 是 Spring 在应用层封装,每次执行完成后通过一次性 schedule() 递归提交,这就意味着错过的时间点会直接丢弃,不会补执行。

总结

ScheduledThreadPoolExecutor 作为继承自 ThreadPoolExecutor 的线程池,是 专门用于定时、周期性任务调度的。

对比维度 ThreadPoolExecutor(普通线程池) ScheduledThreadPoolExecutor(调度线程池)
用途 通用异步任务执行,处理批量任务 延迟任务、周期性定时任务调度
继承关系 父类 继承 ThreadPoolExecutor
7 大参数 全部由调用者自由指定 固定 4 个参数,仅开放 corePoolSize、线程工厂、拒绝策略
工作队列 普通阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue 等,FIFO 固定自研 DelayedWorkQueue,最小堆,按任务到期时间排序
线程创建规则 核心线程 → 队列满 → 创建非核心线程,超过 maximumPoolSize 触发拒绝 队列永远无界,不会创建任何非核心线程,最多 corePoolSize 个核心线程
空闲线程回收 非核心线程超时后回收 无非核心线程,不存在非核心线程回收
任务封装 普通 Runnable / Callable 封装为 ScheduledFutureTask,携带时间、周期、堆索引信息
任务提交逻辑 execute:核心线程 → 队列 → 非核心线程 → 拒绝 delayedExecute:先入队,再按需创建 Worker
任务执行结束 任务执行完成直接销毁 周期任务无异常时,自动重新计算时间,再次入队循环调度
拒绝策略触发时机 队列满且达到最大线程数;或者线程池 shutdown 后提交任务 只有线程池 shutdown 后提交新任务才触发;任务再多也不会触发拒绝,只会堆积,有 OOM 风险
Spring 对应组件 ThreadPoolTaskExecutor(@EnableAsync 异步池) ThreadPoolTaskScheduler(@EnableScheduling、@Scheduled 底层)

附:UML类图

java 复制代码
@startuml
interface Future<V>{}
interface ScheduledFuture<V> extends Delayed, Future {}
interface RunnableFuture<V> extends Runnable, Future{}
interface RunnableScheduledFuture<V> extends RunnableFuture, ScheduledFuture {}
@enduml
相关推荐
大哥43091 小时前
索引 mapping 与代码对不上:一次不报错的静默失效排查
后端
神魔皆以血嗜1 小时前
Java 存储金额
java·后端
大哥43091 小时前
模型名停用与分层降级:一次 LLM 链路的失败面设计
后端
橙子家1 小时前
对象状态与操作合法性异常【C# Exception 详解系列(二)】
后端
架构技术专栏2 小时前
为什么 AI 公司都想做Personal Agent ?
后端
用户761016088472 小时前
黑马RabbitMQ 从入门到可靠性:收发、持久化、死信延迟一篇打通
后端
Bazingga2 小时前
Transformer与自注意力机制:一台"全员检索"机器的说明书
人工智能·后端
星辰云-2 小时前
# 基于Spring Boot的德州特产扒鸡商城系统(毕设|源码|论文|演示视频)
spring boot·后端·课程设计
武子康3 小时前
同为7/8,错误样本却不同:Agent Grader 分歧复算
人工智能·后端·agent