@Sync 注解导致系统OOM 原理探索

引论

今天发现了一篇讲多线程避坑的文章,是一位技术大佬写的,我也是非(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类,这个类是实现了BeanFactoryAwareBeanFactoryAware,那么我们就可以知道他执行了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

在这里插入图片描述

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值