文章目录
1. 文章要点
2. @Async的基本使用
这个注解的作用在于可以让被标注的方法异步执行,但是有两个前提条件
+ 配置类上添加 @EnableAsync
注解
+ 需要异步执行的方法的所在类由Spring管理
+ 需要异步执行的方法上添加了 @Async
注解
我们通过一个Demo体会下这个注解的作用吧
第一步,配置类上开启异步:
@EnableAsync
@Configuration
@ComponentScan("com.dmz.spring.async")
public class Config {
}
第二步,
@Component // 这个类本身要被Spring管理
public class DmzAsyncService {
@Async // 添加注解表示这个方法要异步执行
public void testAsync(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("testAsync invoked");
}
}
第三步,测试异步执行
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
DmzAsyncService bean = ac.getBean(DmzAsyncService.class);
bean.testAsync();
System.out.println("main函数执行完成");
}
}
// 程序执行结果如下:
// main函数执行完成
// testAsync invoked
通过上面的例子我们可以发现,DmzAsyncService
中的 testAsync
方法是异步执行的,那么这背后的原理是什么呢?我们接着分析
3. 原理分析
详情见原文
@Async注解的就是通过AsyncAnnotationBeanPostProcessor这个后置处理器生成一个代理对象来实现异步的,接下来我们就具体看看 AsyncAnnotationBeanPostProcessor
是如何生成代理对象的,我们主要关注一下几点即可:
- 是在生命周期的哪一步完成的代理?
postProcessAfterInitialization
方法中 - 切点的逻辑是怎么样的?它会对什么样的类进行拦截?
- 通知的逻辑是怎么样的?是如何实现异步的?
4. 导致的问题及解决方案
4.1 问题1:循环依赖报错
就像在这张图里这个读者问的问题,
分为两点回答:
第一:循环依赖为什么不能被解决?
这个问题其实很简单,在 《面试必杀技,讲一讲Spring中的循环依赖》这篇文章中我从两个方面分析了循环依赖的处理流程
- 简单对象间的循环依赖处理
- AOP对象间的循环依赖处理
按照这种思路,@Async
注解导致的循环依赖应该属于AOP对象间的循环依赖,也应该能被处理。但是,重点来了,解决AOP对象间循环依赖的核心方法是三级缓存,如下:
在三级缓存缓存了一个工厂对象,这个工厂对象会调用 getEarlyBeanReference
方法来获取一个早期的代理对象的引用,其源码如下:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
// 看到这个判断了吗,通过@EnableAsync导入的后置处理器
// AsyncAnnotationBeanPostProcessor根本就不是一个SmartInstantiationAwareBeanPostProcessor
// 这就意味着即使我们通过AsyncAnnotationBeanPostProcessor创建了一个代理对象
// 但是早期暴露出去的用于给别的Bean进行注入的那个对象还是原始对象
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
看完上面的代码循环依赖的问题就很明显了,因为早期暴露的对象跟最终放入容器中的对象不是同一个,所以报错了。
4.2 问题一解决方案
就以上面读者给出的Demo为例,只需要在为B注入A时添加一个 @Lazy
注解即可
@Component
public class B implements BService {
@Autowired
@Lazy
private A a;
public void doSomething() {
}
}
这个注解的作用在于,当为B注入A时,会为A生成一个代理对象注入到B中,当真正调用代理对象的方法时,底层会调用getBean(a)去创建A对象,然后调用方法,这个注解的处理时机是在
org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency
方法中,处理这个注解的代码位于org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver#buildLazyResolutionProxy
,这些代码其实都在我之前的文章中分析过了
《Spring杂谈 | Spring中的AutowireCandidateResolver》
《谈谈Spring中的对象跟Bean,你知道Spring怎么创建对象的吗?》
所以本文不再做详细分析
4.3 问题2:默认线程池不会复用线程
我觉得这是这个注解最坑的地方,没有之一!我们来看看它默认使用的线程池是哪个,在前文的源码分析中,我们可以看到决定要使用线程池的方法是org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor
。其源码如下:
protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
AsyncTaskExecutor executor = this.executors.get(method);
if (executor == null) {
Executor targetExecutor;
// 可以在@Async注解中配置线程池的名字
String qualifier = getExecutorQualifier(method);
if (StringUtils.hasLength(qualifier)) {
targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
}
else {
// 获取默认的线程池
targetExecutor = this.defaultExecutor.get();
}
if (targetExecutor == null) {
return null;
}
executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?
(AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));
this.executors.put(method, executor);
}
return executor;
}
最终会调用到 org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor
这个方法中
protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}
可以看到,它默认使用的线程池是 SimpleAsyncTaskExecutor
。我们不看这个类的源码,只看它上面的文档注释,如下:
主要说了三点
- 为每个任务新起一个线程
- 默认线程数不做限制
- 不复用线程
就这三点,你还敢用吗?只要你的任务耗时长一点,说不定服务器就给你来个OOM。
4.4 解决方案
最好的办法就是使用自定义的线程池,主要有这么几种配置方法
- 在之前的源码分析中,我们可以知道,可以通过
AsyncConfigurer
来配置使用的线程池
如下:
public class DmzAsyncConfigurer implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
// 创建自定义的线程池
}
}
- 直接在@Async注解中配置要使用的线程池的名称
如下:
public class A implements AService {
private B b;
@Autowired
public void setB(B b) {
System.out.println(b);
this.b = b;
}
@Async("dmzExecutor")
public void doSomething() {
}
}
@EnableAsync
@Configuration
@ComponentScan("com.dmz.spring.async")
@Aspect
public class Config {
@Bean("dmzExecutor")
public Executor executor(){
// 创建自定义的线程池
return executor;
}
}
5. @Lazy的说明
引用自:https://blog.csdn.net/m0_43448868/article/details/112005140
-
当Bean的依赖注入完成后,其属性注入的实际上是一个使用JDK动态代理或者CGLIB创建出来的代理对象,只有用户去调用目标对象的方法时,才会触发去真正完成依赖解析
-
当将@Lazy注解添加到字段或者方法上的参数上,IoC容器将会为其创建代理对象,如果在Bean上面添加@Lazy注解,并且在每个依赖该Bean的地方都添加上@Lazy注解,那么该Bean不会在IoC容器初始化的时候就进行实例化,只有到调用该Bean的方法时,IoC容器才会真正的去实例化Bean
-
如果仅在Bean上面添加@Lazy注解,其它依赖该Bean的地方不添加@Lazy注解并且每个依赖该Bean的Bean并非全部都被@Lazy注解标记,那么即使添加了@Lazy注解,也依然会在IoC容器初始化的时候进行实例化
-
说起来有点绕口,如果有小伙伴未能理解,可以根据以上代码多测试几次就会明白,实在不明白,欢迎在评论区给我留言哦
6. 总结
本文主要介绍了Spring中异步注解的使用、原理及可能碰到的问题,针对每个问题文中也给出了方案。希望通过这篇文章能帮助你彻底掌握 @Async
注解的使用,知其然并知其所以然