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注解默认的线程池。

相关推荐
激流丶13 分钟前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
Themberfue17 分钟前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
让学习成为一种生活方式34 分钟前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
晨曦_子画40 分钟前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
假装我不帅1 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
南宫生1 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
神仙别闹1 小时前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
Heavydrink1 小时前
HTTP动词与状态码
java
ktkiko111 小时前
Java中的远程方法调用——RPC详解
java·开发语言·rpc