Spring--@Async解析

一、@Async 简介

从Spring3开始提供了@Async注解,被该注解标注的方法,Spring底层会新建一个线程池或者使用已有的线程池中的线程去异步的执行被标注的方法。

二、@Async 工作原理

@Async与@Transactional 工作原理基本是一样的,也是通过Spring AOP动态代理去实现的。Spring容器启动初始化bean时,判断类中是否使用了@Async注解,如果使用了则为其创建代理对象,在线程调用@Async注解标注的方法时,通过代理对象执行切入点处理器invoke方法,将方法的执行提交给线程池中的另外一个线程来处理,从而实现了异步执行。

三、在SpringBoot中的使用

@Async在SpringBoot中的使用还是挺简单的,只需要下面两部操作就可以实现方法的异步调用。

1. 在启动类上使用@EnableAsync注解
bash 复制代码
@SpringBootApplication
@EnableAsync
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
2. 将需要异步执行的方法所在的类注入到spring容器,并在异步的方法上添加@Async注解
bash 复制代码
@Component
public class AsyncTest {

    /**
 	*  无返回值调用
 	*/
    @Async
    public void asyncDoSomething(){
        // 具体业务逻辑处理
		......
    }

	/**
 	* 有返回值的调用,使用Future来接收异步任务执行的结果
 	* 对于返回值是Future,需要我们在方法中捕获异常并处理,或者在调用方在调用Futrue.get时捕获异常进行处理
 	* 
 	* @return
 	*/
	@Async
	public Future<String> asyncDoSomethingToReturn() {
    	Future<String> future;
    	try {
        	// 具体业务逻辑处理
        	......
        	future = new AsyncResult<String>("success");
    	} catch (InterruptedException e) {
        	future = new AsyncResult<String>("InterruptedException");
    	} catch(IllegalArgumentException e){
        	future = new AsyncResult<String>("IllegalArgumentException");
    	}
    	return future;
	}
}

四、@Async失效场景

  1. 非公有方法:注解标注方法修饰符为非public时,异步任务会失效。因为@Async是基于动态代理实现的,如果被@Async标注的方法的修饰符不是public的话,将不会对该bean进行代理对象创建或者不会对方法进行代理调用,从而也就无法将任务交由现场池中的某个线程去异步执行任务。
  2. final 修饰的方法:如果动态代理使用的是cglib实现的话,由于cglib实现动态代理的方式主要是靠运行期动态创建目标类的子类,从而无法创建代理对象,也就无法将任务交由现场池中的某个线程去异步执行任务。
  3. 类内部访问:在同一个类中调用被@Async方法调用时,也会因为在当前类中无法生成的代理对象来调用,只能调用实际的目标方法,也就无法将任务交由现场池中的某个线程去异步执行任务。

五、@Async 底层源码解析

5.1 @Async注解源码

从上面@Async注解源码可以看出,只有一个value属性,这个属性就是线程池Bean的名称,如果在注解中设置了这个值,则根据这个值从Spring容器中获取这个线程池的Bean去异步执行方法;如果没设置,则会使用默认的线程池去异步执行该方法。所以这里想要指定具体的线程池去异步执行方法的话,只需要在@Async("taskExecutor")中设置线程池的bean的名称当作注解的value属性值就好。

5.2 @EnableAsync注解源码

(1)根据上面@EnableAsync源码可以看出,该注解中使用了一个@Import注解导入了一个AsyncConfigurationSelector.class类。

(2)进入到AsyncConfigurationSelector类,可以看到该类继承了AdviceModeImportSelector抽象类,AdviceModeImportSelector抽象类又实现了ImportSelector接口。并且在selectImports()方法中根据不同的AdviceMode来选择和返回适当的导入语句。

(3)进入到ProxyAsyncConfiguration配置类中,可以看到会创建一个AsyncAnnotationBeanPostProcessor对象,并设置相关的属性后返回该对象。

(4)再进入到AsyncAnnotationBeanPostProcessor类中,有个setBeanFactory()方法,在这个方法中往BeanFactory容器中增加了一个AsyncAnnotationAdvisor增强器。

(5)进入这个AsyncAnnotationAdvisor增强器的构造函数可以看到,分别创建了一个通知以及切入点。

(6)在这个创建增强方法的的函数中,可以看到创建了一个拦截器,主要通过这个拦截器来拦截标注有@Async注解的方法。

(7)进入到AnnotationAsyncExecutionInterceptor拦截器,可以看到其继承了AsyncExecutionInterceptor,主要的拦截方法就在这个AsyncExecutionInterceptor父类的invoke()方法中。主要步骤也就下面三步:1. 获取线程池;2. 创建线程;3. 提交任务到线程去执行。

(8)看下获取线程池的处理逻辑。主要步骤是:1.先从缓存中去查找,如果缓存中存在线程池就直接返回这个线程池去使用;2.如果缓存中没找到再根据方法的@Async注解value值进行判断;3.方法的的@Async注解设置了value值,则把value的值当做线程池的bean名称去Spring容器中找到这个bean并返回去使用;4.如果方法的@Async注解没有设置了value值,则使用默认的线程池去调用。

(9)看下获取注解值的方法。主要逻辑就是先拿到方法的Async注解,再根据拿到的注解获取该注解的value属性值。

(10)再看下使用默认线程池的方法。主要是先调用父类的getDefaultExecutor方法去获取线程池对象,如果能获取到就直接使用该线程池对象,如果获取不到就新建个SimpleAsyncTaskExecutor线程池对象。

进入SimpleAsyncTaskExecutor的execute方法去创建线程执行任务。

在doExecute()方法中每次都会创建一个线程去执行。

由于SimpleAsyncTaskExecutor是个简单的线程池,通过这个线程池创建的线程不能重复使用,且每次调用都会去创建一个线程,在高并发或者访问很频繁的场景并不适用,很容易造成系统线程资源耗尽,严重可能导致系统崩溃。所以在使用@Async注解去实现方法的异步调用时最好使用自定义线程池或者ThreadPoolTaskExecutor线程池去替换掉@Async注解默认的线程池。

相关推荐
波波00742 分钟前
ASP.NET Core 健康检查实战:不只是一个 /health 接口
后端·asp.net
小码哥_常1 小时前
Spring Boot 搭建邮件发送系统:开启你的邮件自动化之旅
后端
石榴树下的七彩鱼2 小时前
图片修复 API 接入实战:网站如何自动去除图片水印(Python / PHP / C# 示例)
图像处理·后端·python·c#·php·api·图片去水印
我叫黑大帅2 小时前
为什么TCP是三次握手?
后端·网络协议·面试
我叫黑大帅2 小时前
如何排查 MySQL 慢查询
后端·sql·面试
techdashen2 小时前
Rust项目公开征测:Cargo 构建目录新布局方案
开发语言·后端·rust
一 乐2 小时前
电影院|基于springboot + vue电影院购票管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·电影院购票管理管理系统
恼书:-(空寄2 小时前
JVM GC 日志分析 + 常见 GC 场景 + 实战参数调优
java·jvm
消失的旧时光-19432 小时前
Spring Boot 实战(五):接口工程化升级(统一返回 + 异常处理 + 错误码体系 + 异常流转机制)
java·spring boot·后端·解耦
Rust研习社2 小时前
Rust 智能指针 Cell 与 RefCell 的内部可变性
开发语言·后端·rust