注解@Scheduled和@Async笔记
@Scheduled
常见的定时任务时间间隔包括:
- @Scheduled(fixedDelay = xxx):从上一次方法执行完成后,等待指定的时间间隔后再次执行方法。
- @Scheduled(fixedRate = xxx):表示多少毫秒执行一次任务,是按照固定的速率执行任务而不管前一次任务是否已经执行完毕。
- @Scheduled(cron = "xxx"):基于cron表达式指定定时任务执行时间。
在Spring Boot中,所有通过@Scheduled注解定义的定时任务都会被放入一个公用的线程池中执行,默认情况下,该线程池的并发线程数为1,即所有的任务会使用同一个线程被依次执行。
具体来说,当一个定时任务到达其触发时间时,Spring Boot将会从线程池中获取一个线程来执行该任务。如果同一时间有多个任务同时触发,那么这些任务将会按照任务定义的顺序依次排队,等待线程池中的线程被释放后依次执行。
我们可以通过在application.properties
或application.yml
文件中设置spring.task.scheduling.pool.size
属性来配置线程池中的线程数。例如,可以设置该参数的值为2,默认为1,以便在执行任务时使用多个线程,提高任务并行度和执行效率。
从该配置项可以追踪到其对应的自动配置类 TaskSchedulingAutoConfiguration
,部分源码如下:
@ConditionalOnClass({ThreadPoolTaskScheduler.class})
@AutoConfiguration(
after = {TaskExecutionAutoConfiguration.class}
)
@EnableConfigurationProperties({TaskSchedulingProperties.class})
public class TaskSchedulingAutoConfiguration {
public TaskSchedulingAutoConfiguration() {
}
// 当指定名称的Bean存在于Spring容器中时,才创建当前Bean。
// 当前Bean是一个名为 taskScheduler 的任务调度器
@Bean
@ConditionalOnBean(
name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
)
@ConditionalOnMissingBean({SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class})
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()); // 默认是1,可通过上文中的配置进行修改
TaskSchedulingProperties.Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); // 线程前缀名:scheduling-
builder = builder.customizers(taskSchedulerCustomizers);
return builder;
}
...
}
解释:注意事项:
1、在多个任务共享同一个线程池的情况下,如果其中一个任务出现长时间阻塞或者执行时间过长的情况,可能会影响下一个任务的执行时间或者与后续任务的并行执行。因此,建议在编写任务代码时,尽量保证任务能够在较短的时间内执行完毕,并且不会出现阻塞的情况。如果任务代码中有可能出现阻塞或者长时间执行的操作,可以将这些操作放到异步线程中执行,避免对主线程造成阻塞影响其他任务的执行。
2、在 Spring 中,如果一个类中使用了 @EnableScheduling 注解开启了定时任务的支持,那么这个类中定义的所有带有 @Scheduled 注解的方法都会被自动注册为任务,并按照给定的执行规则周期性地执行。
也就是说当其他类中也定义了带有 @Scheduled 注解的方法时,虽然这些方法不在 @EnableScheduling 注解所在的类中,但它们同样被自动注册为任务,并可以按照指定的规则执行。
这是因为 Spring 会自动扫描整个应用程序的类,并自动注册所有具有 @Scheduled 注解的方法。
因此,当您在一个类中使用 @EnableScheduling 注解时,即使其他类中也定义了带有 @Scheduled 注解的方法,只要它们被成功注册到 Spring 容器中,它们也可以被执行。
【当然,一般我们只在启动类上使用@EnableScheduling注解】
3、taskExecutor
线程池的配置是在TaskSchedulingProperties
类中完成的,这个类使用 spring.task.scheduling
前缀进行配置,包含了很多线程池相关细节的配置选项。如下所示:
# 任务调度线程池配置项
# 线程前缀名
spring.task.scheduling.thread-name-prefix=scheduling-
# 执行器大小
spring.task.scheduling.pool.size=1
# 执行程序应等待剩余任务完成的最长时间。
spring.task.scheduling.pool.await-termination-period=30
# 执行器是否应该在关闭时等待计划任务完成。
spring.task.scheduling.pool.await-termination=false
@Async
@Async
是Spring Framework提供的注解之一,用于将方法标注为异步方法并提交到异步任务线程池中执行。当我们在Spring Boot应用程序中使用@Async
注解标注一个方法时,该方法将被异步地执行,即将它放入线程池中后立即返回,而不会等待方法执行完毕再返回。
要使用@Async
注解,我们需要在Spring Boot应用程序中启用异步方法执行功能。启用异步方法执行的最简单方式是在应用程序的主配置类中添加@EnableAsync
注解即可。
注意事项:
在Spring Boot 2.x中,异步执行功能是使用名为
taskExecutor
的线程池(ThreadPoolTaskExecutor
类型)来执行异步任务,而非之前版本中的SimpleAsyncTaskExecutor
。
如果要使用其他的线程池来处理异步方法,可以自定义线程池并作为bean注入到Spring容器中,并使用@Async
注解来指定所需的线程池bean名称或任务执行器对象。例如:@Async(value = "applicationTaskExecutor")
细节一:
当Spring Boot应用程序中没有定义自定义的线程池bean时,Spring Boot应用程序会根据自动配置类注入一个名为applicationTaskExecutor 或 taskExecutor
的线程池对象,它的配置是在TaskExecutionProperties
类中完成的,这个类使用 spring.task.execution
前缀进行配置,包含了很多线程池相关细节的配置选项。如下所示:
# 任务执行线程池(默认配置)
spring.task.execution.pool.core-size=8
spring.task.execution.pool.max-size=Integer.MAX_VALUE
spring.task.execution.pool.queue-capacity=Integer.MAX_VALUE
spring.task.execution.pool.thread-name-prefix=task-
spring.task.execution.timeout.seconds=60
其对应的自动配置类TaskExecutionAutoConfiguration
,部分源码如下:
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
@AutoConfiguration
@EnableConfigurationProperties(TaskExecutionProperties.class)
public class TaskExecutionAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties,
ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
ObjectProvider<TaskDecorator> taskDecorator) {
TaskExecutionProperties.Pool pool = properties.getPool();
TaskExecutorBuilder builder = new TaskExecutorBuilder();
builder = builder.queueCapacity(pool.getQueueCapacity());
builder = builder.corePoolSize(pool.getCoreSize());
builder = builder.maxPoolSize(pool.getMaxSize());
builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
builder = builder.keepAlive(pool.getKeepAlive());
Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator);
builder = builder.taskDecorator(taskDecorator.getIfUnique());
return builder;
}
@Lazy
@Bean(name = { "applicationTaskExecutor","taskExecutor"})
@ConditionalOnMissingBean(Executor.class)
public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
return builder.build();
}
}
细节二:
当我们容器中存在自定义线程池时,applicationTaskExecutor 或 taskExecutor
的线程池对象是不会被创建的。且我们使用@Async
注解没有指定value属性时,项目启动的时候会有这样的提示:
“在上下文中找到多个TaskExecutor bean,并且没有一个名为' taskExecutor'。将其中一个标记为primary或将其命名为'taskExecutor'(可能作为别名),以便将其用于异步处理”,示例:
// 标记为Primary,即主要的线程
@Bean
@Primary
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("my-free-style-");
executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
// 直接起别名为taskExecutor
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("my-free-style-");
executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
而且此时@Async
注解使用的线程池为:SimpleAsyncTaskExecutor
【务必慎用】
SimpleAsyncTaskExecutor
的特点如下:
- 每次执行任务时,都会创建一个新的线程来执行任务,不重用已创建的线程。
- 不会限制线程池的大小,可以无限创建新的线程。
- 不支持对任务的取消操作。
- 在任务完成后,线程不会终止
- 默认情况下,线程都是非守护线程,即不会在主程序退出时自动终止
创建新线程、无限、不重用。这是不是和我们印象中的的线程池不一样,可以说是相悖的,完美躲过线程池优势。
线程池的优势:
- 降低创建线程和销毁线程的性能开销。
- 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行。
- 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题。