一、前言
开发中我们经常会用到异步方法调用,具体到代码层面,异步方法调用的实现方式有很多种,比如最原始的通过实现 Runnable 接口或者继承 Thread 类创建异步线程,然后启动异步线程;再如,可以直接用 java.util.concurrent 包提供的线程池相关 API 实现异步方法调用。
如果说可以用一行代码快速实现异步方法调用,那是不是比上面方法香很多。
Spring 提供了 Async 注解,就可以帮助我们一行代码搞定异步方法调用。Async 注解用起来是很爽,但是如果不对其底层实现做深入研究,难免有时候也会心生疑虑,甚至会因使用不当,遇见一些让人摸不着头脑的问题。
本文首先将对 Async 注解做简单介绍,然后和大家分享一个我们项目中因 Async 注解使用不当的线上问题,接着再深扒 Spring 源码,对 Async 注解底层异步线程池的实现原理一探究竟。
二、Async 注解简介
Async 注解定义源码
从源码可以看出 @Async 注解定义很简单,只需要关注两点:
-
Target ({ElementType.TYPE, ElementType.METHOD}) 标志 Async 注解可以作用在方法和类上,作用在类上时,类的所有方法可以实现异步调用。
-
String value ( ) default "" 是唯一字段属性,用来指定异步线程池,且该字段有缺省值。
Async 注解异步调用实现原理概述
在 Spring 框架中,Async 注解的实现是通过 AOP 来实现的。具体来说,Async 注解是由 AsyncAnnotationAdvisor 这个切面类来实现的。
AsyncAnnotationAdvisor 类是 Spring 框架中用于处理 Async 注解的切面,它会在被 Async 注解标识的方法被调用时,创建一个异步代理对象来执行方法 。这个异步代理对象会在一个新的线程中调用被 @Async 注解标识的方法,从而实现方法的异步执行。
在 AsyncAnnotationAdvisor 中,会使用 AsyncExecutionInterceptor 来处理 Async 注解。AsyncExecutionInterceptor 是实现了 MethodInterceptor 接口的类,用于拦截被 Async 注解标识的方法的调用,并在一个新的线程中执行这个方法。
通过 AOP 的方式实现 Async 注解的异步执行,Spring 框架可以在方法调用时动态地创建代理对象来实现异步执行,而不需要在业务代码中显式地创建新线程。
总的来说,Async 注解的实现是通过 AOP 机制来实现的,具体的切面类是 AsyncAnnotationAdvisor,它利用 AsyncExecutionInterceptor 来处理被 Async 注解标识的方法的调用,实现方法的异步执行。
三、Async 注解底层异步线程池原理探究
获取 Async 注解线程池主流程解析
进入到 Spring 源码 Async 注解 AOP 切面实现部分,我们重点剖析异步调用实现中线程池是怎么处理的。下图是 org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke 方法的实现,可以看出是调用 determineAsyncExecutor 方法获取异步线程池。 AsyncExecutionInterceptor#invoke
下图是 determineAsyncExecutor 方法实现:
上方的图为 AsyncExecutionInterceptor#determineAsyncExecutor,下方的图为 AsyncExecutionAspectSupport#getExecutorQualifier
从代码实现中可以看到 determineAsyncExecutor 获取线程池的大致流程: determineAsyncExecutor 获取线程池流程
如果在使用 Async 注解时指定了自定义线程池比较好理解,如果使用 Async 注解时没有指定自定义线程池,Spring 是怎么处理默认线程池呢?继续深入源码看看 Spring 提供的默认线程池的实现。
Spring 是怎么为 Async 注解提供默认线程池的
Async 注解默认线程池有下面两个方法实现:
-
org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor
-
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#getDefaultExecutor
AsyncExecutionInterceptor#getDefaultExecutor
可以看出 AsyncExecutionInterceptor#getDefaultExecutor 方法比较简单:先尝试调用父类 AsyncExecutionAspectSupport#getDefaultExecutor 方法获取线程池,如果父类方法获取不到线程池再用创建 SimpleAsyncTaskExecutor 对象作为 Async 的线程池返回。
AsyncExecutionAspectSupport#getDefaultExecutor
再来看父类 AsyncExecutionAspectSupport#getDefaultExecutor 方法的实现,可以看到 Spring 根据类型从 Spring 容器中获取 TaskExecutor 类的实例,先记住这个关键点。
我们知道,Spring 根据类型获取实例时,如果 spring 容器中有且只有一个指定类型的实例对象,会直接返回,否则的话,会抛出 NoUniqueBeanDefinitionException 异常或者 NoSuchBeanDefinitionException 异常。
但是,对于 Executor 类型,Spring 容器却 "网开一面",有一个特殊处理:当从 Spring 容器中获取 Executor 实例对象时,如果满足 @ConditionalOnMissingBean (Executor.class) 条件,Spring 容器会自动装载一个 ThreadPoolTaskExecutor 实例对象,而 ThreadPoolTaskExecutor 是 TaskExecutor 的实现类。
上方的图为 TaskExecutionAutoConfiguration,下方的图为 TaskExecutionProperties
从 TaskExecutionProperties 和 TaskExecutionAutoConfiguration 两个配置类我们看到 Spring 自动装载的 ThreadPoolTaskExecutor 线程池对象的参数:核心线程数 = 8;最大线程数 = Integer.MAX_VALUE;队列大小 = Integer.MAX_VALUE。
四、总结
现在 Async 注解线程池源码已经看的差不多了,下面这张图是 Spring 处理 Async 异步线程池的流程: Async 异步线程池获取流程
归纳一下:如果在使用 Async 注解时没有指定自定义的线程池会出现以下几种情况:
-
当 Spring 容器中有且仅有一个 TaskExecutor 实例时,Spring 会用这个线程池来处理 Async 注解的异步任务,这可能会踩坑,如果这个 TaskExecutor 实例是第三方 jar 引入的,可能会出现很诡异的问题。
-
Spring 创建一个核心线程数 = 8、最大线程数 = Integer.MAX_VALUE、队列大小 = Integer.MAX_VALUE 的线程池 来处理 Async 注解的异步任务,这时候也可能会踩坑,由于线程池参数设置不合理,核心线程数 = 8,队列大小过大,如果有大批量并发任务,可能会出现 OOM。
-
Spring 创建 SimpleAsyncTaskExecutor 实例 来处理 Async 注解的异步任务,SimpleAsyncTaskExecutor 不是一个好的线程池实现类,SimpleAsyncTaskExecutor 根据需要在当前线程或者新线程中执行异步任务。如果当前线程已经有空闲线程可用,任务将在当前线程中执行,否则将创建一个新线程来执行任务。由于这个线程池没有线程管理的能力,每次提交任务都实时创建新城,所以如果任务量大,会导致性能下降。