在 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。
七、代码示例(延迟 + 周期任务)
① 延迟任务
ScheduledExecutorService ses = Executors.newScheduledThreadPool(2); ses.schedule(() -> { System.out.println("5 秒后执行"); }, 5, TimeUnit.SECONDS);② 固定速率(FixedRate)
ses.scheduleAtFixedRate(() -> { System.out.println("每 3 秒触发一次,与任务执行时间无关"); }, 0, 3, TimeUnit.SECONDS);③ 固定延迟(FixedDelay)
ses.scheduleWithFixedDelay(() -> { System.out.println("任务执行完后等 3 秒再执行,绝不堆积"); }, 0, 3, TimeUnit.SECONDS);八、ScheduledThreadPoolExecutor 的优点总结
✔ 多线程并行执行定时任务
✔ 使用 DelayQueue 实现精确调度
✔ 任务异常不会影响整个线程池
✔ 支持延迟 + 固定速率 + 固定延迟
✔ 可与 Future 结合获取执行结果
✔ 比 Timer 稳定、安全、功能更强
九、小心周期任务中的“任务堆积”问题
使用scheduleAtFixedRate时:
如果任务执行时间 > 周期
会导致任务连续执行
例如:
scheduleAtFixedRate(task, 0, 1s) task 耗时 3s
那么时间线:
0s: task 执行(耗时 3s)
1s: 时间到了,触发第二次,但任务还没结束
3s: 第一轮结束,立即执行第二轮
这会造成堆积。
十、总结:什么时候用哪种周期任务?
| 场景 | 使用方式 |
|---|---|
| 强调固定时间点执行,如心跳、指标采集 | scheduleAtFixedRate |
| 强调任务稳定、绝不用补课 | scheduleWithFixedDelay |
| 任务有可能阻塞很久 | scheduleWithFixedDelay |
| CPU 占用不可不控 | scheduleWithFixedDelay |
| 系统要尽量保持节奏稳定 | scheduleAtFixedRate |
补充:
ScheduledExecutorService 行为观察 Demo(可直接跑)