前提:看之前我相信你已经掌握如何在springboot项目使用 @Scheduled 开启一个定时任务,以及使用 @Async 开启一个异步任务
说一些你可能不知道:
@Scheduled:同步阻塞任务(单线程)
1. @Scheduled定时任务是同步阻塞任务,因为它所使用的线程池是一个单线程的线程池,这意味着所有任务都是串行执行,只要前一个任务未执行完成,后面的任务都会一直等待下去,并且当一个任务未执行完成,它的下个触发周期会被忽略。
因为这些特点,当我们项目中的定时任务比较密集并且耗时比较长的时候需要特别注意,必要时需要我们提供自己配置的线程池
2.那么这个线程池是哪个呢?很多博客说是ThreadPoolTaskScheduler,并且说可以通过配置 spring.task.scheduling.pool.size 可以改变单线程为多线程,然而当你实际这样去做会发现并没有用,还是单线程同步阻塞任务。当我们点进去TaskSchedulingProperties和TaskSchedulingAutoConfiguration类进去看源码的时候我们会发现
也就是说实际上ThreadPoolTaskScheduler线程池并没有被实例化注册到spring容器,而是优先在TaskExecutionAutoConfiguration注册一个ThreadPoolTaskExecutor(继承自TaskScheduler)的线程池,这个线程池将在@Async的异步任务种被使用,下面会说。
如果我们再点进去看TaskExecutionAutoConfiguration会发现
而且ThreadPoolTaskScheduler与ThreadPoolTaskExecutor都是Executor的子类,随意可以得出结论,@Scheduled的线程池另有他池,并且在springboot的自动根配置中ThreadPoolTaskScheduler与ThreadPoolTaskExecutor只有ThreadPoolTaskExecutor被实例化注册到容器,并且互相干扰。也意味
3.既然不是ThreadPoolTaskScheduler那到底是哪一个?
实际上springboot走到了第五步,我们可以在控制台搜索“No TaskScheduler/ScheduledExecutorService bean found for scheduled processing” 这条打印信息,这部分的源码位于ScheduledAnnotationBeanPostProcessor.finishRegistration()中并且调用了ScheduledTaskRegistrar.afterPropertiesSet(); 这个方法又调用ScheduledTaskRegistrar.scheduleTasks()方法:
ScheduledAnnotationBeanPostProcessor在EnableScheduling中的SchedulingConfiguration被实例化注册到spring容器
在往下看
核心线程为 1,并且队列是无界(DelayedWorkQueue为无界队列)
豁然开朗,一切都明白过来,可以得出结论,在SpringBoot的自动化配置中,会给我们自动初始化一个 核心线程为 1,无界阻塞队列的ScheduledThreadPoolExecutor线程池,所以所有的定时任务都是同步阻塞串行运行的
@Async:异步非阻塞任务(多线程)
1. @Async任务为非阻塞任务,它的所有任务都会提交给springBoot自动给我们创建的一个名为applicationTaskExecutor(别名taskExecutor)类型为ThreadPoolTaskExecutor的线程池(你可以打控制台搜索到这个线程池初始化打印信息)里去执行,
并且当一个任务未执行完成,它的下个触发周期不会被忽略。
2. 通过配置文件我们可以对applicationTaskExecutor进行配置,包括核心线程数,线程名前缀等等,这些配置信息在TaskExecutionProperties bean初始化被注入,并在 TaskExecutionAutoConfiguration 实例化线程池被应用
可以看到配置起作用了
3.具体的流程
源码:
说完springboot自动配置过程,接下来讲如何自定义配置修改这两个线程池
1.ThreadPoolTaskExecutor通过配置文件配置即可,ThreadPoolTaskExecutor默认是允许核心线程过期,可以设置 allow-core-thread-timeout: false 为不允许过期
2.修改@Scheduled任务的线程池,两种方式,两种都配置的话第一种的在@Scheduled会生效
第一种:通过实现SchedulingConfigurer接口的方式,要特别注意这种方式ThreadPoolTaskScheduler不能注册到spring容器成为一个spring bean,否则 ThreadPoolTaskExecutor(@Async)线程池将不会被初始化,如果你忘了为什么,请重新再看一次
@Configuration
public class ScheduleThreadPoolConfig implements SchedulingConfigurer {
@Resource
private TaskSchedulerBuilder builder;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = builder.build();
taskScheduler.initialize();
taskRegistrar.setScheduler(taskScheduler);
}
}
第二种:手动注册 ThreadPoolTaskScheduler 和 ThreadPoolTaskExecutor 进入spring 容器
/**
* @Description 定时任务与异步任务调度线程池初始化
* @auther ccg
* @create 2020-11-27 10:32
*/
@Configuration
public class InitThreadPoolTaskScheduler {
@Bean(name = {"ThreadPoolTaskScheduler"})
public ThreadPoolTaskScheduler ThreadPoolTaskScheduler(TaskSchedulerBuilder builder){
ThreadPoolTaskScheduler taskScheduler = builder.build();
// taskScheduler.initialize();
return taskScheduler;
}
@Lazy
@Bean(
name = {"applicationTaskExecutor", "taskExecutor"}
)
public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
return builder.build();
}
}
注意:如果你仔细观察你会发现,通过第一种方式需要调用线程池 initialize() 方法,而这个方法在ThreadPoolTaskScheduler和ThreadPoolTaskExecutor的共同父类 ExecutorConfigurationSupport 中被定义:
注意图中标记的这几个点,蓝色线部分是不是很熟悉(前面已经说过了),afterPropertiesSet() 是实现 InitializingBean 接口的方法,这个方法将在实例被注册到spring容器时被调用,可以用来初始化bean,而这些工作都是为了初始化真正的线程池ExecutorConfigurationSupport 下的 ExecutorService 成员变量,真正的实例化过程由 ExecutorConfigurationSupport 的抽象方法 initializeExecutor() ,这个方法将被其子类各自实现,ThreadPoolTaskExecutor 为例:
最终的线程池还是回到了jdk的 ThreadPoolExecutor,如此整个流程下来,是不是对springboot的自动化配置又了解了很多, 真棒!!!! 。