在最近的项目中,碰到了@Scheduled注解失效的问题,分析原因后,使用@Scheduled注解做定时任务需求需要格外小心,避免踩入不必要的坑。
比如,有一个需求:一是每隔5s做一次业务处理,另一个则是每隔10s做相应的业务处理,在Springboot项目中,代码如下:
@EnableScheduling
@Component
public class ScheduleTask {
@Scheduled(cron = "0/5 * * * * ?")
public void taskA() {
System.out.println("执行了ScheduleTask类中的taskA方法");
}
@Scheduled(cron = "0/10 * * * * ?")
public void taskB() {
System.out.println("执行了ScheduleTask类中的taskB方法");
}
}
@Component:是将ScheduleTask类注入到Spring容器中。
@Scheduled:表示这个方法是个定时任务
@EnableScheduling:开启定时任务
cron表达式:是一个字符串,字符串以5或6个空格隔开,分开共6或7个域,每一个域代表一个含义,分别为 [秒] [分] [小时] [日] [月] [周] [年]
如果你对cron表达式不太了解,可以在 https://cron.qqe2.com/网站按照自己的需求生成相应的cron表达式。
我创建定时器执行任务目的是为了让它多线程执行任务,但是后来才发现,@Scheduled注解的方法默认是按照顺序执行的,这会导致当一个任务挂死的情况下,其它任务都在等待,无法执行。
那么这是为什么呢?
首先说明一下@Scheduled注解加载的过程,以及它是如何执行的。
解析@Scheduled注解
1. ScheduledAnnotationBeanPostProcessor类处理器解析带有@Scheduled注解的方法
2. processScheduled方法处理@Scheduled注解后面的参数,并将其添加到任务列表中
3. 执行任务。ScheduledTaskRegistrar类为Spring容器的定时任务注册中心。Spring容器通过线程处理注册的定时任务
首先,调用scheduleCronTask初始化定时任务。
然后,在ThreadPoolTaskShcedule类中,会对线程池进行初始化,线程池的核心线程数量为1,
阻塞队列为DelayedWorkQueue。
因此,原因就找到了,当有多个方法使用@Scheduled注解时,就会创建多个定时任务到任务列表中,当其中一个任务没执行完时,其它任务在阻塞队列当中等待,因此,所有的任务都是按照顺序执行的,只不过由于任务执行的速度相当快,让我们感觉任务都是多线程执行的。
下面举例来验证一下,将上述的某个定时任务添加睡眠时间,观察另一个定时任务是否输出。
@Slf4j
@EnableScheduling
@Component
public class ScheduleTask {
private static final ThreadLocal<Integer> threadLocalA = new ThreadLocal<>();
@Scheduled(cron = "0/2 * * * * ?")
public void taskA() {
try {
log.info("执行了ScheduleTask类中的taskA方法");
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Scheduled(cron = "0/1 * * * * ?")
public void taskB() {
int num = threadLocalA.get() == null ? 0 : threadLocalA.get();
log.info("taskB方法执行次数:{}", ++num);
threadLocalA.set(num);
}
}
输出结果:
那么如何解决顺序执行呢?答案是配置定时任务线程池:
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(getExecutor());
}
@Bean
public Executor getExecutor(){
return new ScheduledThreadPoolExecutor(5);
}
}
再次启动观察输出结果:
从输出结果我们可以看到,即使testA休眠,但是testB仍然正常执行,并且其还复用了其它线程,导致执行次数发生了变化。
另外一种情况就是在配置完线程池之后,当你手动修改服务器时间时,目前我做的测试就是服务器时间调前,则会导致注解失效,而服务器时间调后,则不会影响注解的作用。
那么原因是什么呢?
在查询资料后得出:
JVM启动之后会记录当前系统时间,然后JVM根据CPU ticks自己来算时间,此时获取的是定时任务的基准时间。如果此时将系统时间进行了修改,当Spring将之前获取的基准时间与当下获取的系统时间进行比对不一致,就会造成Spring内部定时任务失效。因为此时系统时间发生变化了,不会触发定时任务。
那么这时候怎么解决呢?
1. 重启项目。这在生产环境中肯定是不允许啦,所以Pass
2. 无奈之举,改方案。怎么改呢?就是不适用@Scheduled注解,改成 ScheduledThreadPoolExecutor进行替代。
举例说明:下面是我项目中所写的部分定时任务
ScheduledThreadPoolExecutor 执行流程:
方法说明:
public ScheduledFuture scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
scheduleAtFixedRate方法的作用是预定在初始的延迟结束后,周期性地执行给定的任务,周期长度为period,其中initialDelay为初始延迟。
public ScheduledFuture scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);
scheduleWithFixedDelay方法的作用是预定在初始的延迟结束后周期性地执行给定任务,在一次调用完成和下一次调用开始之间有长度为delay的延迟,其中initialDelay为初始延迟。
来源:
https://www.toutiao.com/i6937161276858647077/
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com