Java 怎么实现定时任务?ScheduledThreadPoolExecutor 和 Timer 有什么区别?
简化版
Java 实现定时/延迟任务主要用 ScheduledThreadPoolExecutor(定时线程池,推荐),它能执行「延迟一段时间后执行」和「周期性重复执行」的任务。它比老式的 Timer 好:① 多线程(Timer 是单线程,一个任务卡住/抛异常会影响所有任务;ScheduledThreadPoolExecutor 是线程池,任务互不影响);② 异常隔离(Timer 里一个任务抛异常会导致整个 Timer 终止、后续任务都不执行;ScheduledThreadPoolExecutor 单个任务异常不影响其他)。它有两个周期方法要分清:scheduleAtFixedRate(固定频率——每隔固定时间执行,不管上次执行多久) 和 scheduleWithFixedDelay(固定延迟——上次执行完后再等固定时间)。底层用「延迟队列(DelayQueue 思想)」——任务按「执行时间」排序,时间到了才出队执行。
详细版
ScheduledThreadPoolExecutor 的用法:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4);
// ① 延迟执行(延迟 5 秒后执行一次)
scheduler.schedule(() -> doTask(), 5, TimeUnit.SECONDS);
// ② 固定频率(首次延迟 1 秒,之后每隔 3 秒执行一次)
scheduler.scheduleAtFixedRate(() -> doTask(), 1, 3, TimeUnit.SECONDS);
// ③ 固定延迟(首次延迟 1 秒,之后每次执行完再等 3 秒)
scheduler.scheduleWithFixedDelay(() -> doTask(), 1, 3, TimeUnit.SECONDS);
Timer vs ScheduledThreadPoolExecutor:
| 维度 | Timer | ScheduledThreadPoolExecutor |
|---|---|---|
| 线程模型 | 单线程 | 线程池(多线程) |
| 一个任务耗时长 | 阻塞后续所有任务 | 不影响其他任务(多线程) |
| 一个任务抛异常 | 整个 Timer 终止 | 只影响该任务、其他正常 |
| 时间基准 | 系统绝对时间(改系统时间受影响) | 相对时间(不受系统时间影响) |
| 推荐 | ❌ 已不推荐 | ✅ 推荐 |
scheduleAtFixedRate vs scheduleWithFixedDelay(高频易混):
scheduleAtFixedRate(固定频率):从"上次开始执行的时间"算下一次
假设周期 3 秒,任务执行耗时 1 秒:
0s 开始 → 3s 开始 → 6s 开始...(每隔 3 秒开始一次,不管执行多久)
⚠️ 如果任务耗时 > 周期(如耗时 4 秒 > 周期 3 秒)→ 会连续执行(没有间隔)
scheduleWithFixedDelay(固定延迟):从"上次执行完的时间"算下一次
假设延迟 3 秒,任务执行耗时 1 秒:
0s 开始~1s 结束 → 等 3 秒 → 4s 开始~5s 结束 → 等 3 秒 → 8s 开始...
(每次执行完再等固定时间,保证任务之间有固定间隔)
⚠️
scheduleAtFixedRate有个坑——如果任务执行时间超过周期,任务会「连续执行」(没有间隔),甚至可能因为「追赶」而堆积。而scheduleWithFixedDelay保证「每次执行完再等固定时间」,任务之间总有间隔、不会堆积。所以如果不希望任务堆积、要保证任务间有间隔,用scheduleWithFixedDelay;只有「必须按固定频率触发」(如每秒采集一次)才用scheduleAtFixedRate(且要保证任务耗时 < 周期)。
完整版教学
一、定时任务的两种需求:延迟与周期
定时任务有两种基本需求,ScheduledThreadPoolExecutor 都支持:
① 延迟执行(一次性):
"延迟 N 秒后执行一次任务"
如:下单后 30 分钟未支付,延迟 30 分钟检查并取消订单
→ schedule(task, delay, unit)
② 周期执行(重复):
"每隔 N 秒重复执行任务"
如:每 5 秒采集一次监控指标、每分钟清理一次过期缓存
→ scheduleAtFixedRate / scheduleWithFixedDelay
ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的子类(继承了线程池能力),额外增加了「定时/延迟调度」能力。它实现 ScheduledExecutorService 接口,提供 schedule(延迟一次)、scheduleAtFixedRate/scheduleWithFixedDelay(周期)三个核心方法。理解「定时任务分延迟一次和周期重复、ScheduledThreadPoolExecutor 是带调度能力的线程池」,就理解了它的定位——它是「线程池 + 定时调度」。
二、为什么用 ScheduledThreadPoolExecutor 而不用 Timer
老式的 Timer 也能做定时任务,但有两个致命缺陷,所以被 ScheduledThreadPoolExecutor 取代:
Timer 的致命缺陷:
① 单线程:Timer 只有一个执行线程
→ 如果一个任务执行时间很长,会阻塞后续所有任务
→ 多个定时任务串行执行,一个慢,全部延迟
② 异常会终止整个 Timer:
Timer 里如果一个任务抛出未捕获的异常
→ 整个 Timer 线程终止!所有后续任务都不再执行(灾难)
→ 一个任务的 bug 导致所有定时任务瘫痪
③ 用系统绝对时间:
Timer 基于 System.currentTimeMillis(系统绝对时间)
→ 如果有人改了系统时间,Timer 的调度会错乱
ScheduledThreadPoolExecutor 解决了这些:① 多线程(线程池,任务并行、互不阻塞);② 异常隔离(单个任务抛异常只影响它自己,其他任务正常——因为它是线程池,每个任务独立);③ 相对时间(用 System.nanoTime 的相对时间,不受系统时间修改影响)。所以「做定时任务用 ScheduledThreadPoolExecutor,别用 Timer」——这是明确的最佳实践。理解「Timer 单线程+异常终止整个 Timer+绝对时间三大缺陷、ScheduledThreadPoolExecutor 多线程+异常隔离+相对时间更好」,就知道了为什么淘汰 Timer。
三、scheduleAtFixedRate vs scheduleWithFixedDelay
两个周期方法的区别是最高频的考点——核心是「从哪个时间点算下一次执行」:
scheduleAtFixedRate(固定频率):
从"本次开始执行的时刻"算下一次执行时刻
下一次 = 本次开始 + 周期
→ 理想情况每隔"周期"就开始一次(固定频率)
周期=3s,任务耗时=1s:
0s开始 → 3s开始 → 6s开始(每 3 秒一次,稳定频率)
scheduleWithFixedDelay(固定延迟):
从"本次执行完的时刻"算下一次执行
下一次 = 本次结束 + 延迟
→ 每次执行完后固定等待"延迟"时间
延迟=3s,任务耗时=1s:
0s开始1s结束 → 等3s → 4s开始5s结束 → 等3s → 8s开始(间隔固定,总周期=耗时+延迟)
关键区别:scheduleAtFixedRate 按「开始时间」算(固定频率,两次开始间隔固定);scheduleWithFixedDelay 按「结束时间」算(固定延迟,执行完到下次开始的间隔固定)。当任务耗时可忽略时两者差不多;当任务耗时不可忽略时区别明显——AtFixedRate 保证「触发频率」(可能连续执行)、WithFixedDelay 保证「执行间隔」(不会连续)。理解「AtFixedRate 从开始算固定频率、WithFixedDelay 从结束算固定间隔」,就分清了这对高频易混的方法。
四、scheduleAtFixedRate 的堆积坑
scheduleAtFixedRate 有个必须警惕的坑——当任务执行时间超过周期时,会导致任务「连续执行」甚至堆积:
scheduleAtFixedRate 周期=3s,但任务实际耗时=5s(超过周期):
0s 开始执行 → 5s 才结束(超过了 3s 的周期)
按理 3s 就该开始下一次,但上一次还没结束(单个任务不会并发执行同一调度)
→ 5s 结束后,立即开始下一次(因为已经"欠"了一次)→ 没有间隔!
→ 如果任务持续超时,会一直"追赶"、连续执行、无间隔
后果:
① 任务之间没有间隔(本想每 3 秒一次,结果连续执行)
② CPU 持续繁忙(一直在执行任务)
③ 极端情况任务堆积(虽然 ScheduledThreadPoolExecutor 对同一个周期任务不会并发,
但连续执行会让系统持续高负载)
这个坑的本质是「AtFixedRate 追求『固定频率』,但任务耗时超过周期时,它无法维持频率,只能连续执行来『追赶』」。所以:① 用 scheduleAtFixedRate 必须保证「任务耗时 < 周期」(否则连续执行、失去间隔);② 如果任务耗时不确定或可能较长,用 scheduleWithFixedDelay(它保证「执行完再等固定时间」、总有间隔、不会连续执行)。实践中 scheduleWithFixedDelay 更安全(不会堆积/连续执行),除非明确需要「固定频率触发」。理解「AtFixedRate 任务超时会连续执行无间隔、WithFixedDelay 保证间隔更安全」,就避开了这个高频坑。
五、底层:延迟队列
ScheduledThreadPoolExecutor 的底层实现用「延迟队列(DelayedWorkQueue,基于堆的延迟队列)」——任务按「下次执行时间」排序:
ScheduledThreadPoolExecutor 的调度原理:
内部用一个"延迟队列"(DelayedWorkQueue,本质是一个按时间排序的优先队列/小顶堆)
队列里的任务按"下次执行时间"排序,最早要执行的在堆顶
工作线程:
从队列头取任务(最早该执行的)
如果还没到执行时间 → 等待(阻塞直到到时间)
到时间了 → 取出执行
如果是周期任务 → 执行后重新计算下次执行时间、放回队列
→ 这就是"延迟队列"的思想(和前面 BlockingQueue 题的 DelayQueue 同源)
底层机制:用一个「按执行时间排序的延迟队列(小顶堆)」,工作线程取堆顶(最早该执行的)任务,没到时间就等、到时间就执行,周期任务执行完重新计算下次时间放回队列。这和 DelayQueue(前面 BlockingQueue 题讲过的「到期才能取的队列」)是同一个思想——「按时间排序、到期才出队」。所以「延迟队列」是定时任务的经典实现基础。理解「ScheduledThreadPoolExecutor 底层是按执行时间排序的延迟队列(堆)、取堆顶到期执行、周期任务重新入队」,就理解了它的实现原理——它是「延迟队列 + 线程池」。
六、定时任务方案的演进
Java 定时任务方案的演进和选型:
单机定时任务的演进:
Timer(已淘汰)→ ScheduledThreadPoolExecutor(推荐的单机方案)
→ Spring 的 @Scheduled(Spring 项目里更方便)
分布式定时任务(多机器要协调,避免重复执行):
单机的 ScheduledThreadPoolExecutor/@Scheduled 在多实例下会重复执行
→ 需要分布式定时任务框架:
Quartz(经典、功能全、支持持久化和集群)
XXL-JOB(轻量、有调度中心和管理界面,国内流行)
Elastic-Job(分片、弹性调度)
选型:
单机简单定时 → ScheduledThreadPoolExecutor / @Scheduled
分布式/需要管理界面/失败重试/分片 → XXL-JOB / Quartz / Elastic-Job
选型逻辑:单机简单定时用 ScheduledThreadPoolExecutor 或 Spring 的 @Scheduled;分布式场景(多实例、要避免重复执行、要管理界面/失败重试/分片)用 XXL-JOB、Quartz、Elastic-Job 等分布式定时框架。核心区别是「单机 vs 分布式」——单机的 ScheduledThreadPoolExecutor 在多实例部署时会「每个实例都执行一遍」(重复执行),分布式框架通过「调度中心统一调度、分片、抢占」解决这个问题。理解「单机用 ScheduledThreadPoolExecutor/@Scheduled、分布式用 XXL-JOB/Quartz、区别是单机 vs 多实例协调」,就掌握了定时任务的完整选型。
记忆钩子:「定时任务用 ScheduledThreadPoolExecutor(推荐):schedule 延迟一次、scheduleAtFixedRate 固定频率(从开始时间算、任务超时会连续执行无间隔的坑)、scheduleWithFixedDelay 固定延迟(从结束时间算、保证间隔更安全);比 Timer 好(Timer 单线程+一个任务异常终止整个 Timer+绝对时间,ScheduledThreadPoolExecutor 多线程+异常隔离+相对时间);底层是按执行时间排序的延迟队列(堆);分布式定时用 XXL-JOB/Quartz」。
七、常见误区与追问
- 误区:定时任务用 Timer 就行。 Timer 有致命缺陷——单线程(一个任务阻塞后续所有)、一个任务抛异常会终止整个 Timer(后续任务全瘫痪)、用绝对时间;应用 ScheduledThreadPoolExecutor(多线程、异常隔离、相对时间)。
- 误区:scheduleAtFixedRate 和 scheduleWithFixedDelay 一样。 AtFixedRate 从「本次开始时间」算下次(固定频率,任务超时会连续执行);WithFixedDelay 从「本次结束时间」算下次(固定间隔,保证任务间有间隔不堆积)。
- 误区:scheduleAtFixedRate 一定每隔固定时间执行。 只在「任务耗时 < 周期」时成立;如果任务耗时超过周期,会连续执行(没有间隔)来「追赶」,失去固定频率;这时应用 scheduleWithFixedDelay。
- 误区:ScheduledThreadPoolExecutor 在多实例部署下不会重复执行。 会——它是单机方案,多实例部署时每个实例都执行一遍;分布式场景要用 XXL-JOB/Quartz 等能协调多实例的框架。
- 追问:Timer 和 ScheduledThreadPoolExecutor 的区别? Timer 单线程(一个任务阻塞后续、一个任务异常终止整个 Timer、用系统绝对时间);ScheduledThreadPoolExecutor 是线程池(多线程并行、单任务异常隔离不影响其他、用相对时间不受系统时间影响),推荐用后者。
- 追问:scheduleAtFixedRate 和 scheduleWithFixedDelay 怎么选? 需要「固定频率触发」(如每秒采集)且任务耗时 < 周期 → AtFixedRate;需要「保证任务间有固定间隔、不堆积」(任务耗时不确定/较长)→ WithFixedDelay(更安全,推荐)。
- 追问:ScheduledThreadPoolExecutor 底层怎么实现调度? 用按「执行时间」排序的延迟队列(DelayedWorkQueue,小顶堆)——工作线程取堆顶(最早该执行的)任务,没到时间就等、到时间就执行,周期任务执行完重新计算下次时间放回队列(延迟队列思想)。
八、加强记忆
Java 定时/延迟任务推荐用 ScheduledThreadPoolExecutor(ThreadPoolExecutor 的子类、带定时调度能力),三个核心方法:schedule(延迟一次)、scheduleAtFixedRate(固定频率)、scheduleWithFixedDelay(固定延迟)。它比老式 Timer 好:Timer 有三大缺陷——单线程(一个任务阻塞后续所有)、一个任务抛异常会终止整个 Timer(后续全瘫痪)、用系统绝对时间;ScheduledThreadPoolExecutor 是线程池(多线程并行)+ 异常隔离(单任务异常不影响其他)+ 相对时间。两个周期方法的区别(高频易混):scheduleAtFixedRate 从「本次开始时间」算下次(固定频率,但任务耗时超过周期会连续执行、无间隔的坑);scheduleWithFixedDelay 从「本次结束时间」算下次(保证任务间固定间隔、不堆积,更安全)——任务耗时不确定就用 WithFixedDelay。底层是按执行时间排序的延迟队列(DelayedWorkQueue 小顶堆)——取堆顶到期执行、周期任务重新入队。选型:单机用 ScheduledThreadPoolExecutor/@Scheduled、分布式(多实例避免重复执行)用 XXL-JOB/Quartz/Elastic-Job。一句话「定时任务用 ScheduledThreadPoolExecutor(比 Timer 好:多线程+异常隔离+相对时间),AtFixedRate 固定频率(超时连续执行坑)/WithFixedDelay 固定间隔更安全,底层延迟队列,分布式用 XXL-JOB」。