@Async你知道多少?
我们都知道 @Async
是一个异步注解,用于在线程池异步执行任务,但是你真的了解其原理吗?
先来一个demo:
1)controller
package com.zxh.controller; import com.zxh.service.TestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") public class TestController{ @Autowired private TestService testService; @GetMapping("/test") public void test(){ for (int i = 0; i < 100; i++) { testService.test(i); } } }
2)service
package com.zxh.service; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Component @Slf4j public class TestService { @Async public void test(Integer i) { if (i % 2 == 0) { log.info("是偶数"); } else { log.info("是奇数"); } } }
3)在启动类添加注解,开启异步
4)启动后访问http://localhost:8081/api/test,打印日志如下:
那么这里我们几乎是0配置,就能实现异步,且这些线程名称都是以 task-
开头,使用的是默认的线程池配置。如果仔细看日志会发现,这些名称最多直到8。那么是否可以推测出线程池默认的核心线程数是8?那么最大线程数又是多少呢?队列最大长度?(疑问1)
我们带着疑问打开这个注解
在注解的注释上有一些关键信息(已通过箭头方式标记)
首先可以看出这个注解可用在类和方法上。用在类上时,等价于在类中的所有方法上添加该注解。
对于①,简单来说就是对于目标方法,入参支持任何类型,但是!其返回值类型只限于 void
和 Future
。既然限制了返回值只有两种,如果我返回String会是什么结果呢?那就来试一下
就此得出结论,如果异步方法的返回值不是void和Future,那么最终的返回值都是null。那么为什么会这样呢,这个疑问稍后在源码中继续寻找答案(疑问2)。
既然返回String时返回值是null,那么我就是需要此类型怎么办呢?那就来到了②,意思就是说如果需要特定的返回值,那么可以使用 AsyncResult
对象封装一下。看到这个是不是一脸懵逼,啥意思?该怎么使用呢?且看改造后的代码
其实说白了,就是通过AsyncResult 对象封装后返回Future,然后通过get()方法来获取参数。所以说,通过AsyncResult 封装后也是返回的Future类型,如果不按此规则返回,那么返回的值是null,有空指针分险!
接下来看注解中的属性,只有一个参数value,根据③注释可以知道,就是指定使用哪个执行器来执行异步任务(即指定线程池的名称)。通过value()点进去后发现只有一个地方使用这个值,其他地方都是注释的引用
方法类路径
org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor#getExecutorQualifier
进入后打断点进行调试(调用时如果没有进入断点则需要重启服务,目前我还不清楚为啥是这样,应该怎么解决?)
进入这个断点时继续F8进行调试,会进入另一个类
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor
执行逻辑是先根据传入的方法获取执行器,由于没有指定执行器,故执行器是null。下面的逻辑就清晰了
根据方法获取@Async的value值,也就是执行器bean的名称(即①)。如果传入的执行器名称不为空,则从spring容器中获取目标执行器(即②),反之则获取默认配置的执行器(即③)。
获取到执行器后,在executors中维护方法和线程池的对应关系,executors是map类型,defaultExecutor是函数式接口Supplier。对于执行器对象信息,后面再细说。
继续F8调试,会进入另一个类,调用invoke方法
org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke
这个invoke()方法是否眼熟?不眼熟也没关系,稍后看是如何来的(疑问3)
其中L38是用来用map中根据method获取线程名称,也就是上一步executors。
L42~L54的代码,主要是封装一个Callable 对象,其内部对于return大致一看是不是就分为两种,分别是L46和L54。仔细看其中的逻辑,在try中先获取我们异步方法的返回值类型,如果是Future类型,会返回其值,反之直接reutrn null。你以为到这里就完了?就可以解释为啥限制了void和Future?那你就特错特错了,其实这里的判断是针对任务设置的,听不懂?那没关系。先看下面的doSubmit()方法。
最终把Callable 对象放到doSubmit()方法中,而doSubmit()方法看字面意思是提交任务,实际上也是这么个意思,就是来执行任务的。
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#doSubmit
这里有四个分支,其中 ListenableFuture 和 CompletableFuture 都继承自 Future ,所以前三个分支就是用来判断是否是 Future 类型的。如果不是 Future 类型,则直接返回null。那么对于源码注释中 “返回值限制是void和Future” 的问题(疑问2)也就迎刃而解了。
对于(疑问3),可以看到类AsyncExecutionInterceptor实现了MethodInterceptor接口,从而来重写的invoke()方法
那么MethodInterceptor又是干嘛用的?MethodInterceptor是一个方法拦截器,用于Spring AOP编程中的动态代理。实现该接口重写invoke方法可对需要增强的方法进行增强。这里的作用就是把使用异步注解的方法提交给线程池执行。
现在再回来看(疑问1),默认配置的核心线程数究竟是多少呢?上面并没有对执行器对象进行说明,这里通过断点可以看出,核心线程数是8,最大线程数 Integer.MAX_VALUE。默认的执行器的bean名称是 applicationTaskExecutor
,线程名称前缀是 task-
,
那么这个类,在哪里配置的呢?既然已经知道了beanName,那么就简单了,按下两次Shift进行搜索
找到这个自动配置类后打断点,重启后会自动进入断点
就是在这里自动注入的默认的配置,可以看出大部分配置都来自properties
org.springframework.boot.autoconfigure.task.TaskExecutionProperties
在这里首先看到了线程名称的前缀配置,其他属性从Pool对象中读取
自此(疑问1)也解决了。
既然上述默认值的配置都看完了,那么如何进行自定义配置线程池参数并指定线程池名称呢?这就又回到了 @Async 注解的 value 属性了,下面修改程序如下
也就是自定义了执行器的配置,将其交给Spring管理。在@Async上标注执行器的beanName。重启后打断点
此时会进入②而不是③,因为此时指定了线程池名称,执行器对象如下,已经变成我们自己配置的值。
那么此时就又出现一个疑问:一个项目共用一个线程池配置按业务使用线程池配置更佳?
其实正常情况下,所有异步的操作共用一个线程池配置是没问题的,但是也不是一概而论。根据实际场景需要,不同的业务最好使用不同的线程池配置,这样不同的业务之间如果队列出现问题不会影响这个服务,降低了耦合性。
上述自定义线程池配置时,是把线程池的配置写在业务类中,在实际开发中是不符合规范的,故需要将其抽取处理,如下
package com.zxh.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; /** * @Auther: zxh * @Date: 20240930 * @Description: 配置线程池 */ @Configuration @EnableAsync public class TaskExecutePool { //核心线程数 private static final Integer CORE_POOL_SIZE = 20; //最大线程数 private static final Integer MAX_POOL_SIZE = 20; //缓存队列容量 private static final Integer QUEUE_CAPACITY = 200; //线程活跃时间(秒) private static final Integer KEEP_ALIVE = 60; //默认线程名称前缀 private static final String THREAD_NAME_PREFIX = "MyExecutor-"; @Bean("MyAsyncTaskExecutor") public Executor myTaskAsyncPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(CORE_POOL_SIZE); executor.setMaxPoolSize(MAX_POOL_SIZE); executor.setQueueCapacity(QUEUE_CAPACITY); executor.setKeepAliveSeconds(KEEP_ALIVE); executor.setThreadNamePrefix(THREAD_NAME_PREFIX); //拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
其实我还发现一个很奇怪的线程,只要我自定义了线程池配置并注入到Spring,如果在使用@Async时不写value的值,那么此时也会使用我们自定义的配置。
那么为什么是这样的呢?那就不得不说执行器的初始化了。
下图中,上面已经说了,如果不配置@Async的value值则会使用默认的执行器(即③)
通过断点可以看出,此时defaultExecutor中配置参数是我们自定义的值
那么defaultExecutor是什么时候初始化?
其实可以看到是在构造方法里面进行赋值的
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#AsyncExecutionAspectSupport
构造方法有两个,那么究竟在哪里调用的?
这就需要注意到 @EnableAsync
注解,只有在启动类或其他配置类添加此注解,异步才会生效,此注解显得尤为重要。那么它是如何做的?
进入注解
org.springframework.scheduling.annotation.EnableAsync
可以看出,这个注解引入了一个异步配置类(即①),这个类在 Spring 容器启动时会被调用,用于选择和导入异步相关的配置类。也需要注意它默认的通知方式是 PROXY
(即②)。
进入这个配置类
org.springframework.scheduling.annotation.AsyncConfigurationSelector#selectImports
上述默认的通知方式是 PROXY
也就是代理模式
那么进入这个类
org.springframework.scheduling.annotation.ProxyAsyncConfiguration
这个类是基于代理实现异步的配置类。
重点就来看AsyncAnnotationBeanPostProcessor类,这里直接创建了对象并注入。类的dragiam关系图如下,主要看 BeanFactoryAware
和 BeanPostProcessor
这两个接口。
其中 "BeanFactoryAware" 只有一个方法
org.springframework.beans.factory.BeanFactoryAware#setBeanFactory
这个方法主要就是用来向Spring容器注入我们自己的类。
而 BeanPostProcessor
作用是在bean初始化之前或之后对bean做一些操作
了解之后我们再回到AsyncAnnotationBeanPostProcessor类,可以看到其实现了 setBeanFactory
方法,
org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor#setBeanFactory
这个方法中关键的是L149,创建了一个异步注解通知类 AsyncAnnotationAdvisor
,然后把这个类注入到Spring工厂中
org.springframework.scheduling.annotation.AsyncAnnotationAdvisor
这个类定义通知和切面。而且上述调用的构造方法如下,分别调用了buildAdvice()和buildPointcut()
这里主要说明buildAdvice()
构造了一个拦截器,再来看看这个拦截器的dragiam关系图
这个类AsyncExecutionAspectSupport是不是似曾相识
最终是调用L92初始化默认执行器。通过断点可以看出在启动时参数defaultExecutor是null,可想而知关键点是getDefaultExecutor()
由于参数beanFactory是null,故返回值也是null,此时defaultExecutor=null。
而对于@Async有自定义线程池配置但value未设置值也会使用自定义的配置的问题,
官方给出的回答是会根据配置的顺序和规则去查找可用的线程池。如果只有一个自定义的线程池被配置,那么即使没有指定 value,也可能会默认使用这个唯一的自定义线程池。
参考:https://www.cnblogs.com/thisiswhy/p/15233243.html