Spring自带定时任务@Scheduled注解实例讲解
作者:心流时间
1. cron表达式生成器
cron表达式生成器:https://cron.qqe2.com/
2. 简单定时任务代码示例:每隔两秒打印一次字符
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service @EnableScheduling public class ScheduleDemo1 { @Scheduled(cron = "*/2 * * * * ?") public static void test() { // 十六进制转换为字符 System.out.print((char) Integer.parseInt("5fc3", 16)); System.out.print((char) Integer.parseInt("6d41", 16)); System.out.print((char) Integer.parseInt("65f6", 16)); System.out.print((char) Integer.parseInt("95f4", 16)); System.out.println(); } }
输出:
3. @Scheduled注解的参数
3.1 cron
参数接收一个cron表达式,cron表达式是一个以空格为间隔符来区分不同域的字符串,总共有6个或7个域。cron表达式从左到右每个域分别标识的[秒] [分] [小时] [日] [月] [周] [年],其中[年]不是必选的域可以省略。
序号 | 域 | 必填 | 值的范围 | 允许的通配符 |
---|---|---|---|---|
1 | 秒 | 是 | 0-59 | , - * / |
2 | 分 | 是 | 0-59 | , - * / |
3 | 时 | 是 | 0-23 | , - * / |
4 | 日 | 是 | 1-31 | , - * ? / L W |
5 | 月 | 是 | 1-12 / JAN-DEC | , - * / |
6 | 周 | 是 | 1-7 or SUN-SAT | , - * ? / L # |
7 | 年 | 否 | 1970-2099 | , - * / |
通配符说明:
- * 表示所有值,例如:在时的字段上设置 *,表示每一个小时都会触发。
- ? 表示不指定值,即当前使用的场景为不需要关心这个字段设置的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为“?”, 具体设置为 0 0 0 10 * ? 。
- - 表示区间,例如:在小时上设置 “10-12”,表示 10,11,12点都会触发。
- , 表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发。
- / 用于递增触发,如在秒上面设置“5/15” 表示从5秒开始,每隔15秒触发(5,20,35,50)。在日字段上设置‘1/3’所示每月1号开始,每隔三天触发一次
- L 表示最后的意思,在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是闰年), 在周字段上表示星期六,相当于“7”或“SAT”。如果在“L”前加上数字,则表示该数据的最后一个。例如在周字段上设置“6L”这样的格式,则表示“本月最后一个星期五”。
- W表示离指定日期的最近那个工作日(周一至周五)。例如在日字段上置“15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发。如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,“W”前只能设置具体的数字,不允许区间“-”)。
- #序号(表示每月的第几个周几),例如在周字段上设置“6#3”表示在每月的第三个周六。注意如果指定“#5”,正好第五周没有周六,则不会触发该配置;小提示:‘L’和 ‘W’可以一组合使用。如果在日字段上设置“LW”,则表示在本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同。
示例:
- 每隔5秒执行一次:*/5 * * * * ?
- 每隔1分钟执行一次:0 */1 * * * ?
- 每天23点执行一次:0 0 23 * * ?
- 每天凌晨1点执行一次:0 0 1 * * ?
- 每月1号凌晨1点执行一次:0 0 1 1 * ?
3.2 fixedDelay
上一次执行完成后延迟多久执行下一次,以上一次任务执行的完成时间开始延迟,如:
@Scheduled(fixedDelay = 5000) //上一次执行完成后延迟5s再执行
3.3 fixedRate
固定延迟多久执行下一次任务,不依赖于上一次任务执行成功的时间,如:
@Scheduled(fixedRate= 5000) //上一次执行后延迟5s就开始执行
3.4 initialDelay
启动后延迟多久后执行第一次,可根据场景搭配fixedRate或fixedDelay实现定时调度,如:
@Scheduled(initialDelay = 5000,fixedRate= 300000) //启动后延迟5s执行,之后每次执行时间间隔5min
3.5 fixedDelayString、fixedRateString、initialDelayString等是String类型,支持占位符
如:@Scheduled(fixedDelayString = “${task.fixed-delay}”)
3.6 timeUnit
时间单位,默认毫秒
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
4. 问题:定时器的任务默认是按照顺序执行的,可能导致一些任务无法执行
我创建定时器执行任务目的是为了让它多线程执行任务,但是后来才发现,@Scheduled注解的方法默认是按照顺序执行的,这会导致当一个任务挂死的情况下,其它任务都在等待,无法执行。
@Scheduled注解加载的过程,以及它是如何执行的:
4.1 ScheduledAnnotationBeanPostProcessor类处理器解析带有@Scheduled注解的方法
4.2 processScheduled方法处理@Scheduled注解后面的参数,并将其添加到任务列表中
4.3 执行任务。
ScheduledTaskRegistrar类为Spring容器的定时任务注册中心。Spring容器通过线程处理注册的定时任务
首先,调用scheduleCronTask初始化定时任务。
然后,在ThreadPoolTaskScheduler类中,会对线程池进行初始化,线程池的核心线程数量为1,
private volatile int poolSize = 1;
阻塞队列为DelayedWorkQueue。
因此,原因就找到了,当有多个方法使用@Scheduled注解时,就会创建多个定时任务到任务列表中,当其中一个任务没执行完时,其它任务在阻塞队列当中等待,因此,所有的任务都是按照顺序执行的,只不过由于任务执行的速度相当快,让我们感觉任务都是多线程执行的。
下面举例来验证一下,将上述的某个定时任务添加睡眠时间,观察另一个定时任务是否输出。
import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Slf4j @EnableScheduling @Component public class ScheduleDemo2 { 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();![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/2022840e83f049e2875381196e7c55ea.png) log.info("taskB方法执行次数:{}", ++num); threadLocalA.set(num); } }
输出:可以观察到两个定时任务不是同时执行的,是按顺序执行的
想要避免顺序执行,进行并发,就要配置定时任务线程池:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledThreadPoolExecutor; @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仍然正常执行,并且其还复用了其它线程,导致执行次数发生了变化。
5. 问题:当系统时间发生改变时,@Scheduled注解失效
另外一种情况就是在配置完线程池之后,当你手动修改服务器时间时,目前我做的测试就是服务器时间调前,则会导致注解失效,而服务器时间调后,则不会影响注解的作用。
原因:
JVM启动之后会记录当前系统时间,然后JVM根据CPU ticks自己来算时间,此时获取的是定时任务的基准时间。如果此时将系统时间进行了修改,当Spring将之前获取的基准时间与当下获取的系统时间进行比对不一致,就会造成Spring内部定时任务失效。因为此时系统时间发生变化了,不会触发定时任务。
解决办法:
重启项目
不使用@Scheduled注解,改成ScheduledThreadPoolExecutor进行替代,部分代码:
实际项目中一般使用xxl-job、Quartz等框架,@Scheduled注解会使用的话也是定时更新一些变量的值,大量的定时任务还是使用专门的定时任务框架实现
参考资料:
到此这篇关于Spring自带定时任务@Scheduled注解的文章就介绍到这了,更多相关Spring定时任务@Scheduled注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!