一、@Async 注解介绍:
@Async
注解用于声明一个方法是异步的。当在方法上加上这个注解时,Spring 将会在一个新的线程中执行该方法,而不会阻塞原始线程。这对于需要进行一些异步操作的场景非常有用,比如在后台执行一些耗时的任务而不影响前台响应。
示例:
@Service
public class MyService {
@Async
public void asyncMethod() {
// 异步执行的代码
}
}
在上面的例子中,asyncMethod
方法使用 @Async
注解标记,表示该方法将在一个独立的线程中执行。
二、@Async 底层分析
1、整体思维导图
2、注解底层实现分析
@Async的实现大概分为以下几个步骤:
// 1、目标代理类
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget("DemoImpl bean");
// 2、代理接口
proxyFactory.addInterface("DemoService");
// 3、设置切点
AsyncAnnotationAdvisor.pointcut = @Async注解
// 4、环绕通知处理
AsyncAnnotationAdvisor.advice = AnnotationAsyncExecutionInterceptor拦截器
// 5、切面 = 切点+通知
proxyFactory.addAdvisor("AsyncAnnotationAdvisor");
// 6、生成代理
UserService userService = proxyFactory.getProxy(getProxyClassLoader());
其中设置切点是在AsyncAnnotationAdvisor类里面
环绕通知处理是在AnnotationAsyncExecutionInterceptor这个拦截器去做的,我们主要就分析一下这个拦截器相关的代码,可以看到直接调用了super(defaultExecutor, exceptionHandler),是在它的父类实现的。
我们看看它的父类
这个类有四个方法:invoke()、getExecutorQualifier()、getDefaultExecutor()、getOrder(),我们主要关注Invoke()方法和getDefaultExecutor()方法。
Invoke()方法如下:
它的作用就是会找出标注了@Async注解的方法,然后生成一个Callable对象,并提交给线程池的一个线程来执行,从而实现了该方法的异步执行。
- 通过AOPUtils.getTargetClass(invocation.getThis()) 获取目标对象的类
- 使用
ClassUtils.getMostSpecificMethod()
方法获取指定目标类和方法 - 通过
BridgeMethodResolver.findBridgedMethod()
方法处理桥接方法,确保获取到用户声明的方法。(桥接方法用来解决运行时泛型被擦除问题) determineAsyncExecutor()
方法,根据用户声明的方法确定异步执行器。-
定义一个
Callable
类型的任务,使用 Lambda 表达式创建了一个匿名函数作为任务内容,通过invocation.proceed()
执行目标方法,并处理可能的异常情况。 this.doSubmit(task, executor, invocation.getMethod().getReturnType())
:将任务提交给异步执行器进行处理,并返回执行结果。
getDefaultExecutor()方法如下:
首先尝试获取 TaskExecutor 实现类的 bean 对象,如果能找到且只有一个,则返回该对象;如果找不到或者找到了多个,则会进入 catch 语句块分支,获取 beanName 为 taskExecutor 的 bean对象,如果获取不到,则会创建一个 SimpleAsyncTaskExecutor 线程池对象。
换句话说,如果直接使用 @Async 注解,Spring 就会直接使用 SimpleAsyncTaskExecutor 线程池。那直接使用 SimpleAsyncTaskExecutor 线程池会出现什么问题呢?现在我们进入第三个主题。
三、@Async 存在的问题或注意事项
1、线程池问题
上面说到,如果直接使用 @Async 注解,Spring 就会直接使用 SimpleAsyncTaskExecutor 线程池。
1)让我们看看这个线程池到底是怎么执行的。
可以看到,它是直接 new 一个线程然后直接开启线程。
他既没有重用线程,也没有设置最大线程数,所以在并发量大的时候会产生严重的性能问题!所以一般在生产环境,特别是 toc 的项目,不建议直接使用 @Async 注解,应该使用自定义线程池搭配 @Async 注解一起使用!
2)那如何配置@Async的自定义线程池呢?
在Spring中,我们可以通过实现 AsyncConfigurer 接口或者直接继承 AsyncConfigurerSupport 类来自定义 Async 异步线程池;
线程池可以直接使用 JDK 提供的 ThreadPoolExecutor 或者 Spring 本身也提供的 TaskExecutor 的实现类,Spring的实现类有以下五种,其中使用最多的就是 ThreadPoolTaskExecutor。
- SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
- SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地
- ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
- ThreadPoolTaskScheduler:可以使用cron表达式
- ThreadPoolTaskExecutor:最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装
接下来,我们就以 ThreadPoolTaskExecutor 为例来自定义一个线程池:
2、与 @Transactional 联用问题
当然,使用@Async还有一些别的注意事项,比如与@Transcational联用时,它们在同一个方法上同时使用时可能导致异步失效。这是因为 @Async
通常会使用一个新的线程,而新线程无法继承原始线程的事务上下文。
解决办法是将 @Async
注解放在另外的类或者方法上,确保异步方法被另外的代理类包装。这样,异步方法就能够在独立的线程中执行,同时也能够继承事务上下文。
@Service
public class MyService {
@Async
public void asyncMethodWithTransaction() {
transactionalMethod();
}
@Transactional
public void transactionalMethod() {
// 事务性操作的代码
}
}
3、循环依赖问题
现在有两个类,ServiceA 和 ServiceB 如下:
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
@Async
public void asyncMethod() {
// 异步方法逻辑
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
其中serviceA、serviceB对象之间相互依赖,serviceA和serviceB总有一个会先实例化,而serviceA或serviceB里面使用了@Async注解,会导致循环依赖异常:
org.springframework.beans.factory.BeanCurrentlyInCreationException
在springboot中,以上报错被捕捉,抛出的异常是:
The dependencies of some of the beans in the application context form a cycle
原因
我们知道,spring三级缓存一定程度上解决了循环依赖问题。A对象在实例化之后,属性赋值【opulateBean(beanName, mbd, instanceWrapper)】执行之前,将ObjectFactory添加至三级缓存中,从而使得在B对象实例化后的属性赋值过程中,能从三级缓存拿到ObjectFactory,调用getObject()方法拿到A的引用,B由此能顺利完成初始化并加入到IOC容器。此时A对象完成属性赋值之后,将会执行初始化【initializeBean(beanName, exposedObject, mbd)方法】,重点是@Async注解的处理正是在这地方完成的,其对应的后置处理器AsyncAnnotationBeanPostProcessor,在postProcessAfterInitialization方法中将返回代理对象,此代理对象与B中持有的A对象引用不同,导致了以上报错。
解决办法
- 在A类上加@Lazy,保证A对象实例化晚于B对象
- 不使用@Async注解,通过自定义异步工具类发起异步线程(线程池)
- 不要让@Async的Bean参与循环依赖
@Async循环依赖问题参考博客
:https://cloud.tencent.com/developer/article/1497689
ps:以下是我整理的java面试资料,感兴趣的可以看看。最后,创作不易,觉得写得不错的可以点点关注!