文章目录
- 1. cron表达式生成器
- 2. 简单定时任务代码示例:每隔两秒打印一次字符
- 3. @Scheduled注解的参数
- 3.1 cron
- 3.2 fixedDelay
- 3.3 fixedRate
- 3.4 initialDelay
- 3.5 fixedDelayString、fixedRateString、initialDelayString等是String类型,支持占位符
- 3.6 timeUnit
- 4. 问题:定时器的任务默认是按照顺序执行的,可能导致一些任务无法执行
- 4.1 ScheduledAnnotationBeanPostProcessor类处理器解析带有@Scheduled注解的方法
- 4.2 processScheduled方法处理@Scheduled注解后面的参数,并将其添加到任务列表中
- 4.3 执行任务。ScheduledTaskRegistrar类为Spring容器的定时任务注册中心。Spring容器通过线程处理注册的定时任务
- 5. 问题:当系统时间发生改变时,@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注解会使用的话也是定时更新一些变量的值,大量的定时任务还是使用专门的定时任务框架实现
参考1
参考2
参考3