Timer和TimerTask是Java轻量级单线程定时方案,适合低频非关键任务;但存在异常终止、阻塞卡死、线程泄漏等严重缺陷,现推荐使用ScheduledExecutorService替代。

Timer 和 TimerTask 的基本用法
Java 的 Timer 配合 TimerTask 是最轻量的原生定时方案,适合单线程、低频、非关键任务。它不是为高并发或高精度设计的,底层靠一个单独的后台线程驱动所有任务。
典型写法是继承 TimerTask 实现 run() 方法,再用 Timer.schedule() 或 Timer.scheduleAtFixedRate() 注册执行逻辑:
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
System.out.println("执行一次");
}
}, 1000); // 延迟 1 秒后执行
注意:一旦 TimerTask.run() 抛出未捕获异常,该 Timer 实例会彻底停止,后续所有任务都不会再触发 —— 这是最常被忽略的崩溃点。
schedule() 和 scheduleAtFixedRate() 的区别
两个调度方法表面相似,但对“延迟执行”和“任务耗时超预期”的处理完全不同:
立即学习“Java免费学习笔记(深入)”;
-
schedule():以“上一次实际执行完成时间”为基准计算下一次触发点。如果某次任务耗时 800ms,而间隔设为 500ms,那下次会等当前执行完再延后 500ms 才启动 —— 会自动退避,不会堆积 -
scheduleAtFixedRate():以“首次计划开始时间”为基准,严格按周期推进。同样耗时超限的情况下,它会立即连续触发(甚至并发执行),试图追平节奏 —— 可能导致任务重叠或资源争抢
例如:设定每 500ms 执行一次,某次运行卡住 2 秒,则:
-
schedule()后续只会在 2000+500=2500ms 后执行下一次 -
scheduleAtFixedRate()会在 500ms、1000ms、1500ms、2000ms 四个时间点尝试启动(可能失败或并行)
Timer 的线程安全与生命周期风险
Timer 内部只有一个工作线程,所有任务串行执行,所以任务体本身不用额外同步 —— 但这恰恰掩盖了更严重的问题:
- 一旦某个
TimerTask.run()进入死循环或长时间阻塞,整个Timer就卡死,其他任务永久挂起 -
Timer不会自动关闭,若创建它的对象被回收,但Timer仍在运行,会造成线程泄漏(JVM 无法 GC) - 没有内置的取消依赖机制:调用
timer.cancel()只终止调度,已提交但未开始的任务会被丢弃,正在运行的任务不受影响
正确做法是在不再需要时显式调用 timer.cancel(),且确保 TimerTask 内部不持有外部对象强引用(避免内存泄漏)。
为什么现在更推荐 ScheduledExecutorService
Timer 的缺陷在真实场景中太容易触发:异常终止、单线程瓶颈、无拒绝策略、无任务统计、不支持 Callable。而 ScheduledExecutorService(如 Executors.newScheduledThreadPool(1))直接覆盖全部需求:
- 任务异常不会中断调度器,仅当前任务失败
- 可配置线程数,支持并行定时任务
- 返回
ScheduledFuture,能主动 cancel、判断是否完成、获取结果 - 天然兼容
Callable,支持带返回值和异常传播的任务
迁移成本极低:schedule(Runnable, delay, unit) 接口几乎一致。除非维护遗留代码,否则新项目不该再选 Timer。
真正麻烦的从来不是“怎么写定时任务”,而是“任务挂了谁来兜底、慢了怎么降级、重复了怎么幂等”——Timer 连第一个问题都答不上来。










