Timer 不适合生产环境,因其单线程设计易漏任务、异常导致静默终止;应改用 ScheduledExecutorService,配合 shutdown 管理生命周期,并区分 scheduleAtFixedRate 与 scheduleWithFixedDelay 的语义。

Timer 会漏掉任务,别在生产环境用
Timer 在执行周期性任务时,如果某个任务执行时间超过间隔,后续任务会被阻塞,甚至直接跳过。这不是 bug,是它的设计缺陷——单线程调度,没有容错机制。
常见错误现象:TimerTask 执行中抛出未捕获异常,整个 Timer 线程静默终止,后续所有任务永远不再触发,日志里连报错都没有。
- 只适合简单、短时、非关键的定时逻辑(比如本地调试时每秒打个 log)
-
Timer.scheduleAtFixedRate()和Timer.schedule()行为不同:前者按“计划时间点”对齐,后者按“上一次执行结束时间”推算下一次 - JDK 1.5+ 就不推荐了,官方文档明确建议换用
ScheduledExecutorService
ScheduledExecutorService 是标准解法,但得配对 shutdown
它用线程池管理调度,支持多任务并发、异常隔离、灵活的拒绝策略,是 Java 并发包里真正靠谱的定时方案。
使用场景:心跳上报、缓存刷新、定时补偿、批量拉取外部数据等真实业务。
立即学习“Java免费学习笔记(深入)”;
- 创建时优先用
Executors.newScheduledThreadPool(1),别用newSingleThreadScheduledExecutor()—— 后者返回的实例无法强制关闭内部线程 - 必须显式调用
scheduler.shutdown()或scheduler.shutdownNow(),否则 JVM 无法退出(尤其在 Spring Boot 测试或命令行工具里容易卡住) - 任务抛异常不会中断调度,但异常堆栈默认被吞掉;想看到日志,得重写
ThreadFactory或包装Runnable捕获打印
scheduleAtFixedRate 和 scheduleWithFixedDelay 的区别很实在
这两个方法名字像,但语义完全不同,选错会导致任务节奏完全失控。
假设任务耗时 800ms,间隔设为 1s:
-
scheduleAtFixedRate():从第一次 scheduled time 开始,每隔 1s 触发一次。第 2 次会在 t=1000ms 触发,哪怕第 1 次还没执行完(会排队等);如果任务总超时,可能连续触发多个任务 -
scheduleWithFixedDelay():等上一次任务 执行完毕 后,再等 1s 才开始下一次。第 2 次最早在 t=1800ms 启动,节奏稳定,适合有状态或资源竞争的任务
示例:scheduler.scheduleWithFixedDelay(task, 0, 5, TimeUnit.SECONDS) 表示“首次立即执行,之后每次执行完等 5 秒再执行下一轮”。
Spring 中 @Scheduled 更省事,但底层还是 ScheduledExecutorService
如果你用 Spring,@Scheduled 看似自动,其实依赖 TaskScheduler 实现,默认就是 ScheduledThreadPoolTaskScheduler,本质还是封装了 ScheduledExecutorService。
容易踩的坑:
- 没加
@EnableScheduling,注解完全不生效,也没报错 - 方法不是 public,或者有参数,
@Scheduled直接忽略(不报错也不执行) - 同一个 Bean 里多个
@Scheduled方法,共用一个线程池;一个方法卡死,其他也全堵住 —— 需要自定义TaskScheduler调大线程数或拆分 Bean - cron 表达式里的
?和*别混用,0 0/5 * * * ?合法,0 0/5 * * * *会抛IllegalArgumentException
复杂点在于:调度精度本身受 JVM GC、系统负载影响,别指望毫秒级准时;真需要高精度,得上 Quartz 或外部调度系统。










