前文三篇文章从 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