Spring Boot 合理使用 @Async 注解开启异步

前言

@Async 注解是 SpringBoot 提供的一种非常简单的异步处理方式。因为简单,所以会忽略到一些使用细节。今天我们重新认识一下@Async又不是不能用变为更合理的用.

简单应用

当前 Spring boot 版本 2.7.21

# 启动类添加 @EnableAsync 注解,以开启异步处理能力

package io.github.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

}

# 调用方法添加 @ Async 注解实现异步

package io.github.test.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class TestController {

    @Async
    @RequestMapping("test")
    public void test() {
        log.info("处理耗时任务test......结束" + Thread.currentThread().getName());
    }
}

 # 执行结果分析

并发200压测后观察日志. 发现来回就9个线程再跑. task-1....task-9.可我们并没有配置线程池?为什么日志表现是池化调用的样子? 接着往下看...

Spring Boot 内置线程池  ThreadPoolTaskExecutor

debug 发现 @Async 底层执行逻辑是先在 BeanFactory 中寻找 TaskExecutor 线程池,找到就直接用. 找不到就创建新线程(SimpleAsyncTaskExecutor).

@Nullable
protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
    if (beanFactory != null) {
        try {
            // 1. 先通过class类型寻找
            return (Executor)beanFactory.getBean(TaskExecutor.class);
        } catch (NoUniqueBeanDefinitionException ex) {
            this.logger.debug("Could not find unique TaskExecutor bean. Continuing search for an Executor bean named 'taskExecutor'", ex);

            try {
                // 2.找不到再通过bean名称来找
                return (Executor)beanFactory.getBean("taskExecutor", Executor.class);
            } catch (NoSuchBeanDefinitionException var4) {
                if (this.logger.isInfoEnabled()) {
                    // 注意这个提醒,注意这个提醒,注意这个提醒,最后会提到
                    this.logger.info("More than one TaskExecutor bean found within the context, and none is named 'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly as an alias) in order to use it for async processing: " + ex.getBeanNamesFound());
                }
            }
        } catch (NoSuchBeanDefinitionException ex) {
            this.logger.debug("Could not find default TaskExecutor bean. Continuing search for an Executor bean named 'taskExecutor'", ex);

            try {
                return (Executor)beanFactory.getBean("taskExecutor", Executor.class);
            } catch (NoSuchBeanDefinitionException var5) {
                this.logger.info("No task executor bean found for async processing: no bean of type TaskExecutor and no bean named 'taskExecutor' either");
            }
        }
    }

    return null;
}

protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
    Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
    // 3. 都找不到池才会创建线程
    return (Executor)(defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}

那已存在的 taskExecutor 怎么来的?真相就在 TaskExecutionAutoConfiguration 配置类中. SpringBoot 大哥初始化时就提前造好了一个叫 taskExecutor 的线程池. 

结论

    对于 SpringBoot 项目来说,直接使用 @Async 并不会每次都创建新线程来执行。而是交给内置的 TaskExecutor 线程池来执行调度。

为什么我使用 @Async 就每次都创建新线程?就消耗我?

// 前提:项目中开发者众多,业务细分多,所以有定义了多个池
@Configuration
public class PoolConfig {

    @Bean("myTaskScheduler")
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("poolScheduler");
        scheduler.setPoolSize(50);
        return scheduler;
    }

    @Bean("asyncHelpsExecutor")
    public Executor asyncHelpsExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(50);
        executor.setMaxPoolSize(1000);
        executor.setQueueCapacity(0);
        executor.setThreadNamePrefix("helps-async-");
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}


// 上边 getDefaultExecutor 方法中输出的此提醒:
25-05-26.15:42:23.796 [http-nio-9191-exec-1] INFO  AnnotationAsyncExecutionInterceptor - More than one TaskExecutor bean found within the context, and none is named 'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly as an alias) in order to use it for async processing: [mytaskScheduler, asyncHelpsExecutor]
// 执行日志 
25-05-26.15:42:24.805 [SimpleAsyncTaskExecutor-1] INFO  TestController         - 处理耗时任务test......结束SimpleAsyncTaskExecutor-1
25-05-26.15:42:24.968 [SimpleAsyncTaskExecutor-2] INFO  TestController         - 处理耗时任务test......结束SimpleAsyncTaskExecutor-2
25-05-26.15:42:25.124 [SimpleAsyncTaskExecutor-3] INFO  TestController         - 处理耗时任务test......结束SimpleAsyncTaskExecutor-3
25-05-26.15:42:25.275 [SimpleAsyncTaskExecutor-4] INFO  TestController         - 处理耗时任务test......结束SimpleAsyncTaskExecutor-4
25-05-26.15:42:25.431 [SimpleAsyncTaskExecutor-5] INFO  TestController         - 处理耗时任务test......结束SimpleAsyncTaskExecutor-5
25-05-26.15:42:25.601 [SimpleAsyncTaskExecutor-6] INFO  TestController         - 处理耗时任务test......结束SimpleAsyncTaskExecutor-6

为什么这里是 SimpleAsyncTaskExecutor 来创建新线程,而非默认线程池。这和之前结论不符!为什么这样?

原因就在日志中的一句话 : More than one TaskExecutor bean found within the context....

大致意思是找到了2个池子,不知如何是好.就都不选.

最终结论

当 @Async 处理异步时,要结合业务细分,建议定义专属线程池,然后@Async("指定专属线程池").避免节外生枝。
// 一定要指定呀
@Async("asyncHelpsExecutor")
@RequestMapping("test")
public void test() {
    log.info("处理耗时任务test......结束" + Thread.currentThread().getName());
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值