调整服务器时间至当前时间前,如何让@Scheduled(cron=xxx)正常运行?
我有23个标注了@Scheduled注解的方法,全部用cron表达式配置定时规则。某天把服务器时间调晚数小时后改回原时间,发现所有@Scheduled方法都停止工作了。
一开始以为是线程阻塞,给所有方法加了@Async注解,但问题没改善,调整时间再回退后,定时任务还是没法运行。通过VisualVM导出线程快照,发现@Async用的timing-pool线程池里的所有线程都处于WAITING状态:
"timingPool-12" #66 prio=5 os_prio=0 tid=0x00000000216de000 nid=0x4fc4 waiting on condition [0x00000000333ce000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000006c3791b28> (a java.util.concurrent.SynchronousQueue$TransferStack) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:458) at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362) at java.util.concurrent.SynchronousQueue.take(SynchronousQueue.java:924) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None ...
推测问题根源是@Scheduled(cron)在服务器时间被调至当前时间之前时出现的逻辑错误。把所有@Scheduled(cron = xxx)改成@Scheduled(fixedDelay = xxx)后,线程没再出现阻塞情况。
现在测试环境需要调整服务器时间验证业务逻辑,想知道怎么确保@Scheduled(cron = xxx)在调整服务器时间(尤其是调至当前时间前)时仍能正常运行?
1. 替换调度器为基于单调时钟的实现
Spring默认的ThreadPoolTaskScheduler依赖系统时间,当时间回退时会导致下一次执行时间计算异常。可以自定义调度器,使用单调递增时钟(基于System.nanoTime())来计算任务间隔,避免系统时间回退的影响:
@Configuration public class SchedulerConfig { @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler() { @Override protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler) { @Override public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { long startNanos = System.nanoTime() + unit.toNanos(initialDelay); return super.scheduleAtFixedRate(() -> { long nowNanos = System.nanoTime(); if (nowNanos >= startNanos) { command.run(); startNanos += unit.toNanos(period); } }, initialDelay, period, unit); } }; } }; scheduler.setPoolSize(10); scheduler.setThreadNamePrefix("custom-scheduler-"); scheduler.initialize(); return scheduler; } }
2. 改用Quartz作为调度框架
Quartz本身对系统时间变化有更好的兼容性,它会定期检查系统时间,当检测到时间回退时会重新计算任务触发时间。只需引入Quartz依赖,配置SchedulerFactoryBean,即可用@Scheduled或Quartz原生API定义任务,无需修改原有cron表达式逻辑。
3. 监听系统时间变化,重启定时任务
在测试环境中,可以添加后台线程监听系统时间,当检测到时间回退超过阈值时,重新注册所有定时任务:
@Component public class TimeChangeListener { @Autowired private ScheduledTaskRegistrar taskRegistrar; private long lastRecordedTime = System.currentTimeMillis(); @PostConstruct public void startTimeMonitor() { new Thread(() -> { while (true) { long currentTime = System.currentTimeMillis(); // 检测到时间回退超过1分钟时触发重启 if (currentTime < lastRecordedTime - 60000) { taskRegistrar.destroy(); taskRegistrar.afterPropertiesSet(); } lastRecordedTime = currentTime; try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }).start(); } }
4. 测试环境用模拟时间工具替代修改系统时间
如果只是为了验证业务逻辑,没必要直接修改服务器时间。可以用PowerMock或自定义时间工具类,替换业务代码中的时间获取逻辑,模拟时间变化,完全避免影响定时任务的调度逻辑。
内容的提问来源于stack exchange,提问作者Criwran

