1. 简介
在Spring中的定时任务是一种强大的功能,它允许开发者在指定的时间点或按照预定的时间间隔或频率执行特定的任务。这些任务可以是各种操作,如自动更新、数据清理、发送通知等。
在Spring框架中,可以通过多种方式来实现定时任务,其中最为常用的是使用@Scheduled注解。这个注解可以添加到方法上,并指定任务的执行周期、延迟等参数。
@Scheduled注解支持多种时间表达式,包括固定速率、固定间隔和Cron表达式等。Cron表达式是一种强大的时间表达式,可以灵活地设置任务的执行时间。例如,你可以使用Cron表达式来设置任务在每天的凌晨0点0分0秒执行。如下使用Cron示例:
public class SchedulingService {
// 每隔5s执行
@Scheduled(cron = "0/5 * * * * ?")
public void s() {
System.out.printf("%s - 执行任务 task-1: %s%n", Thread.currentThread().getName(), new SimpleDateFormat("HH:mm:ss").format(new Date())) ;
try {TimeUnit.SECONDS.sleep(3) ;}
}
}
输出结果
如期输出结果。但会不会有什么其它问题呢?
2. 多任务并发问题
在上面的示例中再添加一个定时任务,如下示例:
// 在上面的示例中,再添加下面的定时任务
// 每隔2s执行一次
@Scheduled(cron = "0/2 * * * * ?")
public void s2() {
System.out.printf("%s - 执行任务 task-2: %s%n", Thread.currentThread().getName(), new SimpleDateFormat("HH:mm:ss").format(newDate())) ;
try {TimeUnit.SECONDS.sleep(1) ;}
}
再次运行
根据执行结果看,2个定时任务都使用的是同一个线程执行的结果也不是预期,所以当你有多个定时任务时要么你错开执行时间,要么配置线程池。
2.1 定时任务默认线程池
本篇文章是基于Spring6环境,下面的类与Spring5.x版本是有差异的,但是大致还是相同。容器启动过程中会对定时任务执行初始化操作(注册 ,配置线程池等)。
public class ScheduledAnnotationBeanPostProcessor implements ApplicationListener<ApplicationContextEvent> {
public void onApplicationEvent(ApplicationContextEvent event) {
// 当Spring容器完成刷新后接收到此事件
if (event instanceof ContextRefreshedEvent) {
/**
* 该方法中做2件事:
* 1. 初始化任务调度线程池
* 2. 执行定时任务
*/
finishRegistration() ;
}
}
private void finishRegistration() {
// 创建ScheduledAnnotationBeanPostProcessor时可以指定线程池
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
} else {
// 默认进入这里
this.localScheduler = new TaskSchedulerRouter();
this.localScheduler.setBeanName(this.beanName);
this.localScheduler.setBeanFactory(this.beanFactory);
this.registrar.setTaskScheduler(this.localScheduler);
}
// ...
}
}
在上面的TaskSchedulerRouter中会从当前容器中查找TaskScheduler或ScheduledExecutorService对象。
public class TaskSchedulerRouter implements TaskScheduler{
private final Supplier<TaskScheduler> defaultScheduler = SingletonSupplier.of(this::determineDefaultScheduler);
protected TaskScheduler determineDefaultScheduler() {
try {
// 查找 TaskScheduler 类型的bean...
// 这里的第三个参数是false,会根据类型查找bean(TaskScheduler)
return resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false);
}
// 当容器中定义了多个TaskScheduler时抛出的异常
catch (NoUniqueBeanDefinitionException ex) {
try {
// 如果存在多个,那么会根据beanName查找(第三个参数为true,怎根据类型和名称查找)
// 默认的beanName = taskScheduler
return resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true);
}
catch (NoSuchBeanDefinitionException ex2) {
// logger
}
}
// 默认容器中没有定义TaskScheduler bean
catch (NoSuchBeanDefinitionException ex) {
// 查找 ScheduledExecutorService 类型的bean
try {
// 第三个参数是false,根据类型查找
return new ConcurrentTaskScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
}
catch (NoUniqueBeanDefinitionException ex2) {
try {
// 同样如果存在多个ScheduledExecutorService类型的bean,会根据类型和名称查找
// 而这个beanName=taskScheduler
return new ConcurrentTaskScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
}
catch (NoSuchBeanDefinitionException ex3) {
// logger
}
}
catch (NoSuchBeanDefinitionException ex2) {
// logger
}
}
// 以上都不存在时,则创建只有一个核心线程的线程池
ScheduledExecutorService localExecutor = Executors.newSingleThreadScheduledExecutor();
this.localExecutor = localExecutor;
return new ConcurrentTaskScheduler(localExecutor);
}
}
到此这你应该清楚了在Spring中默认情况下使用任务调用应用的线程池是只有1个核心线程的线程池,而最大线程数是Integer.MAX_VALUE,使用的队列是个延迟队列,队列容量最大同样是Integer.MAX_VALUE。
2.2 自定义线程池
通过上面的分析,我们可以通过自定义TaskScheduler或ScheduledExecutorService。
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(2) ;
taskScheduler.initialize() ;
return taskScheduler ;
}
// -----------------
@Bean
ScheduledExecutorService taskScheduler() {
ScheduledThreadPoolExecutor taskScheudler = new ScheduledThreadPoolExecutor(2) ;
return taskScheudler ;
}
也可以通过@Scheduled指定线程池;注意:这个scheduler属性是从Spring6.1开始才有
@Scheduled(cron = "0/5 * * * * ?", scheduler = "task1")
2.3 线程池配置
而在SpringBoot环境下,默认配置了一个ThreadPoolTaskScheduler bean
@Bean
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
return builder.build() ;
}
@Bean
@ConditionalOnMissingBean
public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties,
ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
TaskSchedulerBuilder builder = new TaskSchedulerBuilder() ;
// 设置核心线程
builder = builder.poolSize(properties.getPool().getSize()) ;
// ...
return builder;
}
在默认情况下,TaskSchedulingProperties#pool核心线程数是1。
@ConfigurationProperties("spring.task.scheduling")
public class TaskSchedulingProperties{
private final Pool pool = new Pool() ;
public static class Pool {
privateint size = 1 ;
}
}
所以,不管你是Spring环境还是SpringBoot环境下,默认核心线程数都是1。当有多个定时任务的时候需要配置线程池大小,SpringBoot只需要修改配置即可。