深蓝黑背景封面。左侧是一张二维选型坐标系:横轴从「单机」指向「分布式」,纵轴从「低精度 · 海量任务」指向「高精度 · 强规则」;坐标系里散布五个方案点——左下角青色大点是「时间轮」(海量超时、精度粗糙),其上方是「DelayQueue」(延迟队列语义),再往上是「ScheduledExecutor」(单机通用),中上部紫色点是「Quartz」(复杂日历规则),右上角紫色大点是「分布式调度中心」(多机统一管理);每个点旁有一行小字注明一句话定位;坐标系下方一行灰色小字写「没有万能方案:先定量级与精度,再看规则复杂度」。右侧大标题「定时任务怎么实现,Timer、DelayQueue、时间轮、Quartz 怎么选」,副标题「一张选型坐标系:从单机到分布式」,小注「精度、吞吐、规则复杂度,三个维度的取舍」。左上标注「知识延伸 · 定时任务」,右上圆角胶囊标注「Day 05 · 12-02」。 图 1:本文封面,定时任务选型的二维坐标系。

这个系列另有一篇真题解析专门拆解 Netty 时间轮的内部机制,本篇换个视角:把「定时与延迟任务」这个大类的主流方案放到一张坐标系里横向对比,回答工程上更常遇到的问题——手里这批定时任务,到底该用哪个轮子。先给坐标轴:横轴是部署范围(单机到分布式),纵轴是任务特征(低精度海量任务到高精度强规则)。六个方案各就各位,看完机制再看场景。

Timer:教科书里的第一个,也是生产里第一个被淘汰的。

java.util.Timer 是 JDK 最早的定时设施:内部一个二叉堆管理任务,调度一次 O(log n),但所有任务共用一个后台线程串行执行。JDK 文档对它的两个致命伤写得很清楚:一个任务执行太慢会霸占线程、后续任务全部挤压排队;任务抛出未捕获异常,线程终止,整个 Timer 上所有任务跟着陪葬。单线程加无容错,决定了它只配出现在教学示例里。

ScheduledExecutorService:单机定时任务的默认答案。

JDK 文档把 ScheduledThreadPoolExecutor 直接称为「Timer 的更全能替代品」:线程池隔离让单个任务的异常不会拖垮整个调度器(只是该周期任务自身的后续执行会被抑制),schedule 支持任意时间单位,scheduleAtFixedRate 与 scheduleWithFixedDelay 区分了「按固定频率追赶」和「按固定间隔顺延」两种语义。绝大多数单机定时与周期任务——心跳上报、指标采集、定期清理——用它就够了,这是坐标系里覆盖面最广的一个点。

DelayQueue:当你要的不是「定时」而是「延迟后投递」。

DelayQueue 是一个无界阻塞队列:元素实现 Delayed 接口自带到期时刻,堆顶永远是到期最早的那个,take() 会阻塞到有元素到期才返回。JDK 文档的定义很精确——队头是「延迟已过期最久」的元素。它和 ScheduledExecutor 的区别在于语义重心:后者是「到点执行动作」,前者是「到点让数据可被消费」,天然适配订单超时关闭、缓存过期、延迟重试这类「数据等待一个期限」的场景。写个实测看它乱序插入、按到期顺序出队(JDK 21,javac DelayQueueDemo.java && java DelayQueueDemo):

插入顺序: 500ms, 200ms, 800ms
t= 210ms 出队: 缓存过期清理
t= 510ms 出队: 订单超时关闭
t= 808ms 出队: 重试投递

500 ms 的任务先插入却第二个出队,队列语义完全由到期时刻决定。规模上的限制也直观:所有任务驻留在本机内存的堆里,任务量冲到十万级以上时,堆的维护成本和内存占用都开始扎眼。

时间轮:海量、低精度超时场景的专用引擎。

把任务的增删从堆的 O(log n) 压到近似 O(1),代价是精度粗糙到一格 tick——机制细节这篇不展开,真题解析那篇讲得完整。选型上记住它的位置就够了:单机、海量任务、精度要求宽松,典型如网络框架里的连接空闲检测与握手超时,以及 Agent 系统里大批推理请求的过期清理。它不适合搬到「到点必须精确触发」的场景。

Quartz:当触发规则本身变复杂。

前面四个方案的触发条件都是「延迟多久」或「每隔多久」,而「每月最后一个工作日早上九点出账单」这种日历规则它们表达不了。Quartz 的 CronTrigger 用 6 到 7 段的 Cron 表达式描述任意日历规则,官方教程里的例子包括「每月最后一个周五」「每月 15 号最近的工作日」这类精确到历法的触发;再加上持久化 JobStore(任务定义入库,重启不丢)与集群支持,它把定时任务从「代码里的工具」升级成「可运维的调度系统」。代价是重量级:引入框架、建表、维护集群,为几个简单任务上 Quartz 属于杀鸡用牛刀。

分布式调度中心:当任务跨机器、要有人管。

任务量再大一个量级、执行方变成一群服务时,问题从「怎么触发」变成「谁来统一指挥」:中心化调度平台负责分片下发、失败重试、执行日志与可视化运维,执行器注册上来领任务。这是坐标系最右上角的方案,解决的是跨服务大规模批处理的组织问题,而不是单机效率问题。

深蓝黑背景概念图。顶部小字「概念图 · 定时任务选型」,主标题「先定量级与精度,再看规则复杂度」。主体是一张六行对照表,列分别为方案、核心机制、精度与规模、适用场景:第一行 Timer,机制「单线程串行跑所有任务」,精度与规模「一个任务抛异常,全线停摆」,场景「仅教学意义,生产已淘汰」;第二行 ScheduledExecutor,机制「线程池 + 内部延迟队列」,「单机毫秒级够用,单任务异常不拖垮池」,「大多数单机定时 / 周期任务」;第三行 DelayQueue,机制「堆顶是最早到期,take 阻塞等待」,「延迟语义强,规模受内存限制」,「订单超时关闭、缓存过期」;第四行时间轮,机制「哈希分桶 + tick 逐格推进」,「增删 O(1),十万级任务无压力」,「海量超时、心跳、重试」;第五行 Quartz,机制「Cron 表达式 + 持久化 + 集群」,「日历规则最强,精度到秒」,「账单、报表等复杂批任务」;第六行分布式调度中心,机制「中心统一分片下发 + 失败重试」,「多机调度、可视化运维」,「跨服务的大规模批处理」。其中时间轮一行用青色描边高亮,Quartz 与分布式调度中心用紫色标注方案名。底部是决策链面板:标题「先问三个问题:任务量级多大?精度多苛刻?规则多复杂?」,下排五个选择芯片依次写「单机普通定时 → ScheduledExecutor」「要延迟后投递语义 → DelayQueue」「海量超时与心跳 → 时间轮」「复杂日历规则 → Quartz」「跨机器统一管理 → 分布式调度中心」。 图 2:六方案对照表与三问决策链。

决策链:先问三个问题。

任务量级多大?精度多苛刻?规则多复杂?三问答完,方案基本自己浮出来:单机普通定时用 ScheduledExecutorService;要「延迟后投递」语义用 DelayQueue;海量超时与心跳上时间轮;复杂日历规则交给 Quartz;跨机器统一管理才轮到分布式调度中心。注意这条链是「够用即停」——每一档往右都在用复杂度换能力,跳档之前先确认上一档真的撑不住。你项目里的定时任务现在跑在哪一档?有没有过「杀鸡用牛刀」或者反过来被简陋方案坑到的经历?欢迎聊聊。