在 Java 的线程池体系中,ScheduledThreadPoolExecutor 是唯一一个可以执行:
- 延迟任务(delay)
- 周期任务(scheduleAtFixedRate / scheduleWithFixedDelay)
的线程池,也是 Timer 的完全替代品。
本篇文章我们将彻底讲透:
- ScheduledThreadPoolExecutor 的内部结构
- 延迟任务如何组织?
- 周期任务和延迟任务的区别?
- scheduleAtFixedRate 和 scheduleWithFixedDelay 的本质差异
- 为什么 Timer 已经过时?
- 延迟队列 DelayQueue 是如何工作的?
理解这篇,你就真正掌握 Java 定时任务的核心机制。
一、ScheduledThreadPoolExecutor 是什么?
它是 ThreadPoolExecutor 的子类,用于执行定时任务:
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
它具备三种能力:
| 类型 | 方法 | 场景 |
|---|---|---|
| 延迟任务 | schedule() |
延迟执行一次 |
| 固定速率任务 | scheduleAtFixedRate() |
间隔固定时间执行 |
| 固定延迟任务 | scheduleWithFixedDelay() |
上一次结束后,等固定延迟再执行 |
底层全部由一个重要结构支持:
DelayQueue(延迟队列)
二、ScheduledThreadPoolExecutor 内部结构(关键图)
它继承自 ThreadPoolExecutor,但替换了队列类型:
ThreadPoolExecutor
▲
│
ScheduledThreadPoolExecutor
│
使用 DelayedWorkQueue(一个 DelayQueue)
也就是说:
普通线程池使用 BlockingQueue
ScheduledThreadPoolExecutor 使用 DelayedWorkQueue
DelayedWorkQueue 不是普通队列:
- 任务按照"到期执行时间"排序
- 只有到期的任务才能被线程取出执行
- 内部使用了基于最小堆的 优先队列(PriorityQueue)
三、延迟任务底层原理:基于 DelayQueue + 时间轮(类似机制)
当你执行:
service.schedule(task, 5, TimeUnit.SECONDS);
内部做了两件事:
✔ 1. 把任务包装成 ScheduledFutureTask
包含:
-
任务本体
-
下次执行时间(triggerTime)
-
任务序号(用于排序)
✔ 2. 丢进 DelayedWorkQueue(DelayQueue)
DelayQueue 会:
-
按照"执行时间"建立一个小顶堆
-
堆顶永远是最早执行的任务
-
线程从队列取任务时,如果没到时间,会阻塞等待
流程:
当前时间 < 任务触发时间 → 阻塞
当前时间 >= 任务触发时间 → 执行任务
这就是"延迟任务"的底层机制。
四、周期任务底层原理(重点)
Java 提供两种周期任务:
① scheduleAtFixedRate(固定速率)
scheduleAtFixedRate(task, 0, 5, SECONDS);
含义:
不管任务执行多久,每隔 5 秒触发一次。
举例:
- 第 1 次:0s
- 第 2 次:5s
- 第 3 次:10s
- ...
如果一个任务执行 6 秒怎么办?
答案:
下一次任务会"补课"式触发(可能会连着执行)。
也就是说:
-
它不关心任务是否执行完
-
它关心的是时间点是否到了
这容易造成"任务堆积"问题。
② scheduleWithFixedDelay(固定延迟)
scheduleWithFixedDelay(task, 0, 5, SECONDS);
含义:
任务执行完后,等 5 秒再执行下一次。
举例:
-
任务执行 6 秒
-
等待 5 秒
-
下一次在 11 秒执行
执行时间取决于任务执行时长。
五、两者区别(面试必问)
| 方法 | 固定点执行? | 与任务执行时长有关? | 是否可能任务堆积? |
|---|---|---|---|
| scheduleAtFixedRate | ✔ 是 | ❌ 否 | ✔ 可能堆积 |
| scheduleWithFixedDelay | ❌ 否 | ✔ 是 | ❌ 不会堆积 |
一句话总结:
FixedRate :按点执行(补课式)
FixedDelay:执行完再延迟(绝不堆积)
六、为什么 Timer 已经过时,必须使用 ScheduledThreadPoolExecutor?
Timer 的缺点非常致命:
❌ 1. Timer 只有一个线程,任务串行执行
❌ 2. Timer 中的一个异常会导致整个调度线程退出
❌ 3. Timer 的时间精度差,在系统时间变化时会出错
❌ 4. 不支持多线程执行任务
相比之下:
| Timer | ScheduledThreadPoolExecutor |
|---|---|
| 单线程 | 多线程 |
| 任务阻塞会导致全部延迟 | 任务可并行执行 |
| 异常会导致整个 Timer 停止 | 不会导致线程池崩溃 |
| 时间精度差 | 使用 System.nanoTime,更精确 |
因此:
在所有实际项目中,都必须使用 ScheduledThreadPoolExecutor 替代 Timer。
七、代码示例(延迟 + 周期任务)
① 延迟任务
cpp
ScheduledExecutorService ses = Executors.newScheduledThreadPool(2);
ses.schedule(() -> {
System.out.println("5 秒后执行");
}, 5, TimeUnit.SECONDS);
② 固定速率(FixedRate)
cpp
ses.scheduleAtFixedRate(() -> {
System.out.println("每 3 秒触发一次,与任务执行时间无关");
}, 0, 3, TimeUnit.SECONDS);
③ 固定延迟(FixedDelay)
cpp
ses.scheduleWithFixedDelay(() -> {
System.out.println("任务执行完后等 3 秒再执行,绝不堆积");
}, 0, 3, TimeUnit.SECONDS);
八、ScheduledThreadPoolExecutor 的优点总结
-
✔ 多线程并行执行定时任务
-
✔ 使用 DelayQueue 实现精确调度
-
✔ 任务异常不会影响整个线程池
-
✔ 支持延迟 + 固定速率 + 固定延迟
-
✔ 可与 Future 结合获取执行结果
-
✔ 比 Timer 稳定、安全、功能更强
九、小心周期任务中的"任务堆积"问题
使用 scheduleAtFixedRate 时:
-
如果任务执行时间 > 周期
-
会导致任务连续执行
例如:
cpp
scheduleAtFixedRate(task, 0, 1s)
task 耗时 3s
那么时间线:
0s: task 执行(耗时 3s)
1s: 时间到了,触发第二次,但任务还没结束
3s: 第一轮结束,立即执行第二轮
这会造成堆积。
十、总结:什么时候用哪种周期任务?
| 场景 | 使用方式 |
|---|---|
| 强调固定时间点执行,如心跳、指标采集 | scheduleAtFixedRate |
| 强调任务稳定、绝不用补课 | scheduleWithFixedDelay |
| 任务有可能阻塞很久 | scheduleWithFixedDelay |
| CPU 占用不可不控 | scheduleWithFixedDelay |
| 系统要尽量保持节奏稳定 | scheduleAtFixedRate |
补充: