引论
今天发现了一篇讲多线程避坑的文章,是一位技术大佬写的,我也是非(zou)常(ma)认(guan)真(hua)的看完了这篇文章(文章链接如下),其中的第8点吸引了我,平时都是直接用的,并没有去真正了解他是如何实现的,这居然都有坑。我截取了文章的部分内容如下:
我当时觉得不应该呀,因为公司中也确实有挺过地方是直接使用@Async注解的,也从来没有因为线程太多而造成OOM情况,但是大佬也不太可能会欺骗我们,带着这个问题我去研究了一下@Async的实现。
探索
@Async
注解大家应该用的挺多了吧,该注解使用需要配合@EnableAsync
,这时候可能有同学有疑问,为啥@EnableAsync
这个注解可以控制@Async
是否生效呢?原因就是在@EnableAsync
中进行对应异步服务的配置和生成,如果这些异步服务都没有生成,那自然@Async
也使用不了了。那么探索的第一步非常明确了,就是去瞅瞅@EnableAsync
干了些啥?
@EnableAsync 作用
进入到这个注解中, 我直呼好家伙,注释都写了一百多行,按照我对于这些注解的通用经验来看,一定是需要去看下@Import
中填入的AsyncConfigurationSelector
类了。
org.springframework.scheduling.annotation.AsyncConfigurationSelector
注释的意思就是这个类用来根据EnableAsync#mode
的配置来选择对应的AbstractAsyncConfiguration
实现,EnableAsync#mode
一般我们也不会去单独设置,其默认值就是PROXY
类型,最后就是返回了ProxyAsyncConfiguration
类,咋看样子是到头了呀,那么接下来程序该怎么走呢?不慌,我们可以注意到AsyncConfigurationSelector
实现了AdviceModeImportSelector
org.springframework.context.annotation.AdviceModeImportSelector
哦豁,这个类是实现在springframework.context
,这一定是一个非常牛逼的类,通过注释我们可以知道,这个类一般是工作于@Ebable***
注解中的,在@Enable***
注解中一般可以添加一个参数为mode,这个参数的值为AdviceMode
类型的,这样就可以根据这个mode值的不同,来动态的选择具体的实现类名称。而返回的实现类名称用来干啥呢?那就是进来进行类的初始化并装载进入Spring容器中。具体实现的地方是在spring的初始化工作中。
org.springframework.context.annotation.ConfigurationClassParser#processImports
这一块我就不展开了,不然文章也太长了。
通过上面的分析我们可以知道,最终ProxyAsyncConfiguration
被初始化进入容器中,并执行了。
org.springframework.scheduling.annotation.ProxyAsyncConfiguration
这个类是创建了AsyncAnnotationBeanPostProcessor
这个类,AsyncAnnotationBeanPostProcessor
又是一种特殊类哇,具体又多特殊呢
就是他最终实现了BeanPostProcessor
,了解过Spring启动过程就可以发现实现了BeanPostProcessor
的类会被优先实现并创建出来,而实现了***Aware
的接口会在启动的时候自动调用某些方法。
- 如果实现了
BeanFactoryAware
,那么就会自动调用setBeanFactory
方法 - 如果实现了
BeanClassLoaderAware
,那么就会自动调用setBeanClassLoader
方法 - 如果实现了
BeanNameAware
,那么就会自动调用setBeanName
方法
不要问我咋知道这些的。代码里发现的~~
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#invokeAwareMethods
不行了,等会一定要画张图,东西有点多。
那么大家还能回来么,刚才我们聊到ProxyAsyncConfiguration
中创建了AsyncAnnotationBeanPostProcessor
类,这个类是实现了BeanFactoryAware
和BeanFactoryAware
,那么我们就可以知道他执行了setBeanFactory
方法
接下来就是AsyncAnnotationAdvisor
初始化中的操作,这个东东,同学们一定要格外注意,这个类是继承自AbstractPointcutAdvisor
,这个类就是AOP实现里的了,所以创建了这个后会做些啥?
不用考虑,就是对于@Async注解的方法设置切面了。
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#configure
在中间走了一系列操作后,我们来到了这个函数中,这个就是存储异步服务执行器的地方了,将默认的执行器保存起来。
这时有同学就要问了,我在代码里并没有看到有获取@Async
的地方呀,嘿嘿,其实这里只是创建了AsyncAnnotationAdvisor
,advisor是一个绑定了切点的通知。在后置处理器AsyncAnnotationBeanPostProcessor中
这里就是对于类的代理处理位置,具体advisor
这些实现有兴趣可以去瞅一瞅Spring AOP的实现。
@EnableAsync 总结
到这里@EnableAsync
作用就几乎完成了。
@Async 作用
在经过@EnableAsync
之后,对于@Async
注解的方法已经生成了代理,改代理类就是AsyncExecutionInterceptor
,为啥是这个类呢,还记得上面
这个地方来生成了AnnotationAsyncExecutionInterceptor
,该接口就是实现了AsyncExecutionAspectSupport
类,这个类上有invoke
方法,在调用@Async
注解的方法时首先会掉用到这个方法中。
利用异步执行器来执行方法,就是将方法变成了异步了。
@Async 方法多次调用引起OOM问题探究
这里要敲重点了,还记得文章一开始提出的问题嘛,就是@Async
注解的方法在高并发情况下回产生过多的线程,而使得程序发生OOM。
为了验证这个问题,那就需要关注到下面这行代码
我们需要去探究一下到底返回了什么异步执行器,来,直接进入到这个方法中
那我们需要考虑两种场景
- @Async
- @Async(“customerTreadPool”)
也就是@Async
注解上是否有值的情况。
如果@Async
注解上没有值,则执行器会去获取默认的执行器的方法,并调用get()
方法来获取对应的执行器,例如上面截图中的this.defaultExecutor.get()
。不知道同学们还记得上面的这张截图嘛
么错,就是这个地方设置的,其中默认异步执行器传了一个方法getDefaultExecutor(this.beanFacory)
,我们在这里调用这个方法的get()
来获取结果,那么我们就一起来看下这个方法。
这里我把注释截全了,希望大家可以多去看看方法上的注释,能够我们少走很多弯路。通过上面我们可以知道,就是首先去获取Spring
容器中实现TaskExecuor
的bean,如果没有的话,就返回SimpleAsyncTaskExecutor
。我们来看下这个SimpleAsyncTaskExecutor
类执行部分代码
这就是大佬文中指出的问题了,也就是异步执行器为SimpleAsyncTaskExecutor
时,就会发生创建线程数量过多,导致系统OOM的问题,而异步执行器为SimpleAsyncTaskExecutor
的条件有两个
- @Async注解上没有配置对应的线程池执行器
- Spring 容器中没有实现
TaskExecuor
的bean
如何避免这种问题
1、破除第一个条件,在@Async
注解上配置上对应的线程池
// 线程池配置类
@Configuration
public class AsyncPoolConfig {
@Bean
public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(200);
executor.setQueueCapacity(25);
executor.setKeepAliveSeconds(200);
executor.setThreadNamePrefix("asyncThread");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@Service
public class TestService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 在注解上填写对应的线程池执行器
@Async("asyncThreadPoolTaskExecutor")
//@Async
public Future<String> asyncMethod() {
sleep();
logger.info("异步方法内部线程名称:{}", Thread.currentThread().getName());
return new AsyncResult<>("hello async");
}
public void syncMethod() {
sleep();
}
private void sleep() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2、 破除第二个条件,不要使用Spring
,而是使用SpringBoot
;获取容器中先初始化TaskExecuor
的bean。
为什么使用SpringBoot
就可以破解第二个条件,就是SpringBoot
在启动时会自动初始化一个TaskExecuor
的bean
可以看到,SpringBoot
下如果容器中没有发现Executor
的实现类对应的bean,就会自动创建一个ThreadPoolTaskExecutor
,这个默认的异步执行器的配置在TaskExecutionProperties
中
我们可以填写对应的配置来修改这个默认异步执行器的配置。所以我们可以认为在SpringBoot
下要选择SimpleAsyncTaskExecutor
的第二个条件:Spring 容器中没有实现TaskExecuor
的bean,一定不会满足,自然就选不到SimpleAsyncTaskExecutor
,而是会选到对应的异步执行器,而异步执行器的执行方式就是
- 没有到达核心线程数时过来任务就会创建新的线程来处理任务
- 达到核心线程数时会将任务放入到延迟队列中
- 如果延迟队列满了,但是线程数还没有达到最大线程数,就会创建线程来执行任务,直到线程数到达最大线程数
- 如果线程数到达最大线程数后还有任务过来,那么就会执行抛弃策略。
对应的代码位置为
java.util.concurrent.ThreadPoolExecutor#execute