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

102 阅读3分钟

写在前面

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

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);
    }
}
@Component
@Slf4j
public class TaskService {

    @SneakyThrows
    @Async
    public void deel() {
        log.info("Thread Name :{} ", Thread.currentThread().getName());
        Thread.sleep(5000);
    }
}

压测实验

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

关键代码:

image.png AsyncExecutionInterceptor中会调用getDefaultExecutor获取Spring中TaskExecutor,如果没有就会 new SimpleAsyncTaskExecutor()。

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

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

升级SpringBoot到2.1.0.RELEASE重新压测

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

image.png 在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);
    }

image.png

从测试结果来看是没有问题,执行任务使用了项目中配置的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);
    }

image.png 如果项目中配置了多个Executor也会使用SimpleAsyncTaskExecutor

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

总结

  1. SpringBoot 2.1.9 之前版本 使用@Async 如果不指定 Executor 会使用 SimpleAsyncTaskExecutor,每次执行会创建一个新的线程,任务量大了可能会导致OOM, SpringBoot 2.1.0之后版本 引入了 TaskExecutionAutoConfiguration使用 ThreadPoolTaskExecutor作为默认 Executor;
  2. 当项目中有多个 Executor 实列时也会使用 SimpleAsyncTaskExecutor当作默认线程池,建议@Primary指定主Bean;
  3. @Async使用时,最好指定线程池,使用越简单,隐藏问题越多。