• SpringBoot项目@Async默认线程池导致OOM问题?


    写在前面

    最近看到很一篇文章说SpringBoot项目中@Async默认线程池会导致OOM,因为我的项目中也用到@Async注解,所以赶紧看了一下,在网上搜索@Async导致OOM案例还是很多的,于是我我就研究了一下。

    在这里插入图片描述

    Demo项目演示

    使用SpringBoot 2.0.9.RELEASE 创建演示工程,项目比较简单,使用@EnableAsync开启异步,在TaskService中@Async开启方法异步。

    @SpringBootApplication
    @EnableAsync
    @RestController
    public class AsyncDemoApplication {
        @Resource
        private TaskService taskService;
        @GetMapping("/id")
        public String test()  {
            taskService.deel();
            return "ok";
        }
        public static void main(String[] args) {
            SpringApplication.run(AsyncDemoApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    @Component
    @Slf4j
    public class TaskService {
    
        @Async
        public void deel()  {
            log.info("Thread Name :{} ", Thread.currentThread().getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    压测实验

    在这里插入图片描述

    从压测结果上来看,刚一开始等待线程数就有3024条,日志中SimpleAsyncTaskExecutor-11922 编号已达到11922个了,看来确实和网上说的一样 @Async默认线程池是SimpleAsyncTaskExecutor,会每次创建一个线程去执行任务,任务量大了会产生OOM。

    关键代码:

    在这里插入图片描述

    protected void doExecute(Runnable task) {
       Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
       thread.start();
    }
    
    • 1
    • 2
    • 3
    • 4

    在SimpleAsyncTaskExecutor的doExecute方法中每次会创建新线程来执行,所以导致瞬间等待线程数达到3024条,确实有点坑人。

    升级SpringBoot到2.1.0.RELEASE重新压测

    在这里插入图片描述

    升级后结果完全不同了,线程数量并没有上升,线程名也变成task-8 这样的了,看来是SpringBoot 优化掉了这个 SimpleAsyncTaskExecutor

    在这里插入图片描述

    在2.1.0之后的版本多了一个TaskExecutionAutoConfiguration,在项目缺少 Executor Bean的情况下注入了一个ThreadPoolTaskExecutor,作为@Async默认线程池。

    SpringBoot 2.0.9.RELEASE配置线程池后再测试

    
    @SpringBootApplication
    @EnableAsync
    @RestController
    public class AsyncDemoApplication {
    
        @Bean("fileTask")
        public Executor fileTask() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(10);
            executor.setMaxPoolSize(40);
            executor.setQueueCapacity(200);
            executor.setKeepAliveSeconds(60);
            executor.setThreadNamePrefix("file-");
            executor.setWaitForTasksToCompleteOnShutdown(true);
            executor.setAwaitTerminationSeconds(60);
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            return executor;
        }
        @Resource
        private TaskService taskService;
    
        @GetMapping("/id")
        public String test() {
            taskService.deel();
            return "ok";
        }
    
        public static void main(String[] args) {
            SpringApplication.run(AsyncDemoApplication.class, args);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这里插入图片描述

    从测试结果来看是没有问题,使用项目中配置的fileTask

    配置多个线程池测试

    @SpringBootApplication
    @EnableAsync
    @RestController
    public class AsyncDemoApplication {
    
        @Bean("fileTask")
        public Executor fileTask() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(10);
            executor.setMaxPoolSize(40);
            executor.setQueueCapacity(200);
            executor.setKeepAliveSeconds(60);
            executor.setThreadNamePrefix("file-");
            executor.setWaitForTasksToCompleteOnShutdown(true);
            executor.setAwaitTerminationSeconds(60);
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            return executor;
        }
    
        @Bean("imgTask")
        public Executor imgTask() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(10);
            executor.setMaxPoolSize(40);
            executor.setQueueCapacity(200);
            executor.setKeepAliveSeconds(60);
            executor.setThreadNamePrefix("img-");
            executor.setWaitForTasksToCompleteOnShutdown(true);
            executor.setAwaitTerminationSeconds(60);
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            return executor;
        }
    
        @Resource
        private TaskService taskService;
    
        @GetMapping("/id")
        public String test() {
            taskService.deel();
            return "ok";
        }
    
        public static void main(String[] args) {
            SpringApplication.run(AsyncDemoApplication.class, args);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    在这里插入图片描述

    如果项目中配置了多个Executor也会使用 SimpleAsyncTaskExecutor
    在这里插入图片描述

    原因是在AsyncExecutionInterceptor 中调用 getDefaultExecutor() 时beanFactory.getBean(TaskExecutor.class) 找到了两个bean所以 报了 NoUniqueBeanDefinitionException,导致没获取到Executor而使用了SimpleAsyncTaskExecutor,所以在fileTask 添加@Primary可解决多个bean问题。

    总结

    1. SpringBoot 2.1.9 之前版本 使用@Async 不指定 Executor 会使用 SimpleAsyncTaskExecutor,任务量大会导致OOM, SpringBoot 2.1.0之后版本 引入了 TaskExecutionAutoConfiguration使用
      ThreadPoolTaskExecutor作为默认 Executor;
    2. 当项目中有多个 Executor 实列时也会使用 SimpleAsyncTaskExecutor,导致OOM,建议@Primary指定主Bean;
    3. @Async是可以使用的,最好指定线程池,使用越简单,隐藏问题就越多。
  • 相关阅读:
    rpc入门笔记0x01
    【概率论】第八章 假设检验
    zabbix自动发现linux系统挂载的nas盘,并实现读写故障的监控告警
    java 面试题
    Ubuntu24多版本python解释器使用
    创建+注册 子应用_定义路由
    Java笔记八(instanceof,类型转换,static详解,抽象类,接口,内部类以及异常)
    Java多线程(6):锁与AQS(下)
    ZZULIOJ:1003: 两个整数的四则运算
    AD20~PCB的板层设计和布线
  • 原文地址:https://blog.csdn.net/whzhaochao/article/details/126032116