# Spring Boot Async ## Spring Executor和Scheduler的自动配置 当当前上下文中没有Executor类型的bean对象时,spring boot会自动配置一个ThreadPoolTaskExecutor类型的bean对象,并且将该bean对象和异步task执行(@EnableAsync)和spring mvc异步请求处理关联在一起。 该默认创建的ThreadPoolTaskExecutor默认使用8个核心线程,并且线程数量可以根据负载动态的增加或者减少。 可以通过如下方式对ThreadPoolTaskExecutor进行配置: ```properties # 该线程池最多含有16个线程 spring.task.execution.pool.max-size=16 # 有有界队列存放task,上限为100 spring.task.execution.pool.queue-capacity=100 # 当线程空闲10s(默认60s)时会进行回收 spring.task.execution.pool.keep-alive=10s # 设置核心线程数量 spring.task.execution.pool.core-size=8 ``` 如果使用@EnableScheduling,一个ThreadPoolTaskScheduler也可以被配置。该线程池默认使用一个线程,但是也可以动态设置: ```properties spring.task.scheduling.thread-name-prefix=scheduling- spring.task.scheduling.pool.size=2 ``` ## Task Execution and Scheduling Spring通过TaskExecutor和TaskScheduler接口为task的异步执行和调度提供了抽象。 ### Task Execution Abstraction Executor对应的是JDK中线程池的概念。在Spring中,TaskExecutor接口和java.util.concurrent.Executor接口相同,该接口中只有一个execute方法(execute(Runnable task)),接受一个task。 ### TaskExecutor接口的实现种类 Spring中包含许多预制的TaskExecutor实现类,该实现类如下: - SyncTaskExecutor:该实现类不会执行异步的调用,所有任务的执行都会发生在调用该executor的线程中。(通常使用在不需要多线程的场景) - SimpleAsyncTaskExecutor:该实现类不会复用任何的线程,相反的,对于每次调用该executor,都会使用一个全新的线程。但是,该实现类的确支持对并发数量的限制,该executor会阻塞任何超过并发数量限制的调用,直到slot被释放为止。(SimpleAsyncTaskExecutor并不支持池化技术) - ConcurrentTaskExecutor:该实现类是java.util.concurrent.Executor实例的adapter,其可以将java.util.concurrent.Executor实例的配置参数以bean properties的形式暴露。(当ThreadPoolTaskExecutor的灵活性无法满足需求时,可以使用ConcurrentTaskExecutor) - ThreadPoolTaskExecutor:该实现类型是最广泛被使用的。该实现类可以通过bean properties来配置java.util.concurrent.ThreadPoolExecutor实例,并且将该实例wrap在TaskExecutor实例中。当想要使用另一种java.util.concurrent.Executor时,可以使用ConcurrentTaskExecutor。 - DefaultManagedTaskExecutor:该实现使用了通过JNDI获取的ManagedExecutorService ### TaskExecutor的使用 在springboot中,当使用@EnableAsync时,可以通过配置ThreadPoolExecutor的bean properties来配置线程池的核心线程数和最大线程数等属性。 ```properties # 该线程池最多含有16个线程 spring.task.execution.pool.max-size=16 # 有有界队列存放task,上限为100 spring.task.execution.pool.queue-capacity=100 # 当线程空闲10s(默认60s)时会进行回收 spring.task.execution.pool.keep-alive=10s # 设置核心线程数量 spring.task.execution.pool.core-size=8 ``` ## Spring TaskScheduler Abstraction 为了在特定的时间点执行task,Spring引入了TaskScheduler接口,接口定义如下: ```java public interface TaskScheduler { ScheduledFuture schedule(Runnable task, Trigger trigger); ScheduledFuture schedule(Runnable task, Instant startTime); ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period); ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period); ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay); ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay); ``` > scheduleAtFixedRate和scheduleAtFixedDelay区别: > - fixed rate:表示两个task执行开始时间的间隔 > - fixed delay:表示上一个task结束时间和下一个task开始时间的间隔 > > 实例如下: > - fixed rate:TTWWWTTTWWT...(开始时间间隔为5) > - fixed delay:TTWWWWWTTTTWWWWWTTTTTTTWWWWWT...(上次结束和下次开始之间的间隔为5) > > **通常,TaskScheduler默认情况下是单线程执行的,故而fixed rate执行时,如果一个Task执行时间超过period时,在当前task执行完成之前,下一个task并不会开始执行。下一个task会等待当前task执行完成之后立马执行。** ### Trigger接口 Trigger接口的核心理念是下次执行事件由上次执行的结果决定。上次执行的结果存储在TriggerContext中。 Trigger接口如下: ```java public interface Trigger { Date nextExecutionTime(TriggerContext triggerContext); } ``` TriggerContext接口如下:(其具有默认的实现类SimpleTriggerContext) ```java public interface TriggerContext { Date lastScheduledExecutionTime(); Date lastActualExecutionTime(); Date lastCompletionTime(); } ``` ### Trigger实现类 Spring为Trigger提供了两个实现类,其中CronTrigger允许task的调度按照cron expression来执行(类似linux中的crond)。 ```java scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI")); ``` Spring的另一个Trigger实现是PeriodicTrigger,其接受一个固定的period期间,一个可选的初始delay值,并接收一个boolean值标识该period是fixed-rate还是fixed-delay。 ### TaskScheduler实现类 如果不需要外部的线程管理,可以使用spring提供的ThreadPoolTaskScheduler,其会将任务委托给ScheduledExecutorService来提供bean-properties形式的配置。 ## 对任务调度和异步执行的注解支持 Spring同时对任务调度和异步方法的执行提供注解支持。 ### 启用Scheduling注解 想要启用@Async和@Scheduled注解,必须将@EnableAsync注解和@EnableScheduling注解添加到一个@Configuration类上。 ```java @Configuration @EnableAsync @EnableScheduling public class AppConfig { } ``` > **@Async注解的实现是通过proxy模式来实现的,故而如果在类内部调用位于同一个类中的@Async方法,那么代理拦截会失效,此时调用的@Async方法将会同步执行而非异步执行** > @Async可以接收一个value值,用于指定目标的执行器(Executor或TaskExecutor的beanName) ### @Scheduled注解 在将@Scheduled注解标注到方法时,可以为其指定一个trigger的元数据,使用示例如下: ```java @Scheduled(fixedDelay = 5000) public void doSomething() { // something that should run periodically } ``` > 默认情况下,fixedDelay、fixedRate、initialDelay的时间单位都是ms,可以指定timeUnit来指定其他的时间单位: > ```java > @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) > public void doSomething() { > // something that should run periodically > } > ``` 可以为@Scheduled注解使用cron表达式 ```java @Scheduled(cron="*/5 * * * * MON-FRI") public void doSomething() { // something that should run on weekdays only } ``` ### @Async注解 通过为方法指定@Async注解,可以让该方法异步执行,该方法的执行通过TaskExecutor。 ```java @Async void doSomething() { // this will be run asynchronously } ``` 可以为@Async标注的方法指定参数和返回值,但是异步方法的返回值只能为void或是Future类型。 在该异步方法的调用方调用返回Future实例的get方法之前,调用方仍然能够执行其他操作,异步方法的执行位于TaskExecutor的线程中。 ```java @Async Future returnSomething(int i) { // this will be run asynchronously } ``` 不能将@Async注解和生命周期回调(例如@PostConstruct)进行混用,如果想要异步的初始化bean对象,需要按照如下方法: ```java public class SampleBeanImpl implements SampleBean { @Async void doSomething() { // ... } } public class SampleBeanInitializer { private final SampleBean bean; public SampleBeanInitializer(SampleBean bean) { this.bean = bean; } @PostConstruct public void initialize() { bean.doSomething(); } } ```