应使用 ScheduledThreadPoolExecutor 替代 Timer:它支持多线程、异常隔离、可配置线程数及 ExecutorService 生态;需显式传 ThreadFactory、避免 scheduleAtFixedRate 执行可能超时任务、务必调用 shutdown、注意 scheduleWithFixedDelay 的延迟逻辑、正确使用 TimeUnit、通过 ScheduledFuture.cancel 安全取消任务、Spring Boot 中优先用 @Scheduled。

用 ScheduledThreadPoolExecutor 替代老旧的 Timer
Java 里最常被误用的定时工具是 Timer,它单线程执行所有任务,一旦某个任务抛出未捕获异常,整个调度器就静默停止——连日志都不打。生产环境几乎没人该用它。ScheduledThreadPoolExecutor 是正确选择:支持多线程、异常隔离、可配置核心线程数,且与 ExecutorService 生态兼容。
-
ScheduledThreadPoolExecutor构造时建议显式传入ThreadFactory,方便打上业务前缀(如"task-scheduler-pool-%d"),避免线程名全是pool-1-thread-1 - 不要调用
scheduleAtFixedRate去执行可能超时的任务——如果上次执行没结束,下一次会立刻在新线程触发,容易堆积;改用scheduleWithFixedDelay,等上一次真正完成后再延时启动 - 务必调用
shutdown()或shutdownNow(),否则 JVM 无法正常退出(线程池默认是 non-daemon)
scheduleWithFixedDelay 的延迟单位和初始延迟陷阱
很多人以为第二个参数 initialDelay 是“第一次执行前等待多久”,其实它就是字面意思:第一次执行前等那么久;但第三个参数 delay 是“上一次执行结束后,再等多久才开始下一次”——不是从调度时间点算起。这点和 scheduleAtFixedRate 的“固定周期”有本质区别。
ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1);
scheduler.scheduleWithFixedDelay(
() -> System.out.println("run at " + System.currentTimeMillis()),
5, // 初始延迟 5 秒
10, // 每次执行完后等 10 秒再执行下一次
TimeUnit.SECONDS
);- 如果任务体耗时 8 秒,那两次打印间隔是 10 秒(8 + 2);如果耗时 12 秒,那间隔就是 12 秒(因为 delay 是“执行完再等”,所以实际间隔 = 执行耗时 + delay)
- 单位必须用
TimeUnit显式指定,不能传毫秒数直接当秒用——schedule(..., 5000, TimeUnit.MILLISECONDS)和schedule(..., 5, TimeUnit.SECONDS)等价,但schedule(..., 5000, TimeUnit.SECONDS)就是 5000 秒
如何安全取消一个已提交的定时任务
调用 schedule* 方法返回的是 ScheduledFuture>,不是普通 Future。它的 cancel(boolean mayInterruptIfRunning) 行为和普通线程中断一致:若任务正在运行,mayInterruptIfRunning = true 会尝试中断线程(仅对响应中断的阻塞操作有效,比如 Thread.sleep、Object.wait、Lock.lockInterruptibly);若任务还没开始,就直接标记为已取消。
- 别依赖
isCancelled()判断任务是否“已被取消”——它只表示cancel()被调用过,不保证任务没执行;要确认是否执行过,得靠外部状态变量或原子计数器 - 如果任务里用了不可中断的 IO(如
FileInputStream.read()),cancel(true)对它无效,只能等它自己结束 - 多个任务共享同一个
ScheduledThreadPoolExecutor时,取消某个任务不会影响其他任务,这是它比Timer更健壮的关键
Spring Boot 里更推荐用 @Scheduled 而不是手写线程池
纯 Java SE 项目才需要手动管理 ScheduledThreadPoolExecutor;Spring Boot 应用应优先使用 @Scheduled 注解,它底层默认复用 TaskScheduler(基于 ScheduledThreadPoolExecutor),且自动处理 Bean 生命周期、异常传播、配置外化(spring.task.scheduling.pool.*)。
立即学习“Java免费学习笔记(深入)”;
- 必须在启动类或配置类上加
@EnableScheduling,否则注解完全不生效 -
@Scheduled(fixedDelay = 5000)对应scheduleWithFixedDelay;@Scheduled(fixedRate = 5000)对应scheduleAtFixedRate;@Scheduled(cron = "0 */5 * * * ?")支持 cron 表达式 - 若需动态启停某任务,不能靠注解本身——得把逻辑封装进 Service,再用
@EventListener监听上下文事件,或暴露 HTTP 接口控制原子布尔开关
实际调度逻辑越简单越好。复杂依赖、数据库事务、HTTP 调用这些,都得考虑失败重试、幂等、分布式锁——定时器本身只是个触发器,别让它承担太多。










