前言:在springboot默认的线程池中,是单一线程。所以默认情况下,所有Scheduled不能并发执行。
这里简单的写了三个方案写法
解决方法都是自定义一个线程池,
一般通常的写法是下面这种,重写SchedulingConfigurer ,使用自定义的Scheduled
方案一、
@Configuration
public class TestConfiguration implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(TaskScheduler());
}
@Bean(destroyMethod = "shutdown")
public Executor TaskScheduler() {
return Executors.newScheduledThreadPool(10);
}
}
但是,在使用期间发现,即使不重写SchedulingConfigurer ,如下代码,依然可以成功的并发执行,
方案二:
@Configuration
public class TestConfiguration {
@Bean(destroyMethod = "shutdown")
public Executor TaskScheduler() {
return Executors.newScheduledThreadPool(10);
}
}
查了很多资料,都没有关于SchedulingConfigurer是否必须重写的区别。只能从EnableScheduling 注解开始翻看源码,进入EnableScheduling 注解,可以发现
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}
可以看见其中引入了一个SchedulingConfiguration,继续进入SchedulingConfiguration.class,最终来到ScheduledAnnotationBeanPostProcessor.class。
在springboot项目启动时,通过EnableScheduling注解,最终来到ScheduledAnnotationBeanPostProcessor这个类,通过其中方法private void finishRegistration(),开始加载加载自定义的线程池。
private void finishRegistration() {
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}
// 1 --- 查找是否有SchedulingConfigurer类型的自定义的bean
if (this.beanFactory instanceof ListableBeanFactory) {
Map<String, SchedulingConfigurer> configurers =
((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
for (SchedulingConfigurer configurer : configurers.values()) {
//1.1 、使用configureTasks重写的方法,为this.registrar赋值,初始化taskScheduler
configurer.configureTasks(this.registrar);
}
}
// 2 ---如果上面 1.1中已经赋值的话,这里this.registrar.getScheduler()!=null,直接跳过
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
try {
// 2.1--- 查找TaskScheduler类型的bean ,false表示不使用名字查找,根据结果初始化taskScheduler
this.registrar.setTaskScheduler(resolveSchedulerBean(TaskScheduler.class, false));
}
catch (NoUniqueBeanDefinitionException ex) {
//2.2 --- 注意异常名字,NoUniqueBeanDefinitionException,不唯一
//如果有多个TaskScheduler类型的bean,则使用名字“taskScheduler”查找,根据结果初始化taskScheduler
try {
this.registrar.setTaskScheduler(resolveSchedulerBean(TaskScheduler.class, true));
}
catch (NoSuchBeanDefinitionException ex2) {
//2.3 ---有多个TaskScheduler类型的bean,且,查询不到name为“taskScheduler”的bean
if (logger.isInfoEnabled()) {
logger.info("More than one TaskScheduler bean exists within the context, and " +
"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
ex.getBeanNamesFound());
}
}
}
catch (NoSuchBeanDefinitionException ex) {
//2.4---如果查不到TaskScheduler类型的bean,则默认查询ScheduledExecutorService类型的bean。根据结果初始化taskScheduler
logger.debug("Could not find default TaskScheduler bean", ex);
// Search for ScheduledExecutorService bean next...
try {
this.registrar.setScheduler(resolveSchedulerBean(ScheduledExecutorService.class, false));
}
catch (NoUniqueBeanDefinitionException ex2) {
//2.5 --- 如果不唯一,则根据名字taskScheduler查询。根据结果初始化taskScheduler
try {
this.registrar.setScheduler(resolveSchedulerBean(ScheduledExecutorService.class, true));
}
catch (NoSuchBeanDefinitionException ex3) {
//2.6 ---如果不唯一,且根据名字也查不到
if (logger.isInfoEnabled()) {
logger.info("More than one ScheduledExecutorService bean exists within the context, and " +
"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
ex2.getBeanNamesFound());
}
}
}
catch (NoSuchBeanDefinitionException ex2) {
//2.7---如果查询不到,ScheduledExecutorService类型的bean
logger.debug("Could not find default ScheduledExecutorService bean", ex2);
// Giving up -> falling back to default scheduler within the registrar...
logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
}
}
}
//2.8
this.registrar.afterPropertiesSet();
}
进入代码最后2.8中的afterPropertiesSet方法:
@Override
public void afterPropertiesSet() {
scheduleTasks();
}
protected void scheduleTasks() {
//如果taskScheduler始终未被初始化,则使用默认的线程池,newSingleThreadScheduledExecutor(),看名字就知道是单线程
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
......
结论,
(一)根据上面代码注释代号,“注释代号”1,解释了文章的方案一,启动时查找SchedulingConfigurer,并通过重写的方法configureTasks ,显式的指定Scheduler,初始化线程池。
(二)在上面注释,2.4到2.7中,解释了,若Scheduler一直没有初始化,则会默认查询ScheduledExecutorService 这个bean,这就解释了,文章开始说的方案二,其中直接返回了一个“return Executors.newScheduledThreadPool(10);”即,ScheduledExecutorService类型。所以,会加载此bean,初始化线程池。
所以,即使不重写SchedulingConfigurer 这个接口,加载器依然能找到需要加载的线程池。
意外收获:
在看源码过程中,发现上面2.1 到2.3中,提供了另一种加载方式,在没有指定SchedulingConfigurer时,则会优先查询 TaskScheduler类型的bean,然后再ScheduledExecutorService 。
所以也可以通过定义TaskScheduler 实现多线程并发定时任务,简单的写法如下
方案三、
@Configuration
public class TestConfiguration {
@Bean
public TaskScheduler taskScheduler(){
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10);
taskScheduler.initialize();
return taskScheduler;
}
}
从这里可以看出加载顺序,SchedulingConfigurer --> TaskScheduler -->ScheduledExecutorService
TaskScheduler 和ScheduledExecutorService 会优先根据类型找,如果存在多个同类型的,则根据默认的名字“taskScheduler”来找
所以,通常会显式的重写SchedulingConfigurer 接口,这样就不会再继续查找后面的TaskScheduler和ScheduledExecutorService了(看注释代号2)
附task
@Service
public class Task {
//5秒一次
@Scheduled(cron = "*/5 * * * * ?")
public void task1() throws InterruptedException {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " " + Thread.currentThread().getName() + " 任务一启动");
Thread.sleep(10000);//任务耗时10秒
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " " + Thread.currentThread().getName() + " 结束");
}
@Scheduled(cron = "*/5 * * * * ?")
public void task2() throws InterruptedException {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " " + Thread.currentThread().getName() + " 任务二启动");
Thread.sleep(10000);
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " " + Thread.currentThread().getName() + " 结束");
}
}
@SpringBootApplication
@EnableScheduling
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class,args);
}
}
测试结果:5秒一次,执行耗时10秒
可以看出,两个任务可以同时执行,同一个任务不会并发执行(对于同一个任务,上一个执行完后,再进行下一次任务,可以看出两个任务都是过了10秒执行完后,等待5秒再次执行,而不是固定的5秒一次)