为什么不建议直接使用@Async

174 阅读6分钟

@Async如果在不设置线程池的情况下,spring会创建SimpleAsyncTaskExecutor的线程池,该线程池每次执行都会创建线程,高并发或者访问很频繁的场景,很容易造成系统线程资源耗尽。

如下文章能够深入了解@Async的机制

一、@Async 简介

从Spring3开始提供了@Async注解,被该注解标注的方法,Spring底层会新建一个线程池或者使用已有的线程池中的线程去异步的执行被标注的方法。

二、@Async 工作原理

@Async与**@Transactional **工作原理基本是一样的,也是通过Spring AOP动态代理去实现的。Spring容器启动初始化bean时,判断类中是否使用了@Async注解,如果使用了则为其创建代理对象,在线程调用@Async注解标注的方法时,通过代理对象执行切入点处理器invoke方法,将方法的执行提交给线程池中的另外一个线程来处理,从而实现了异步执行。

三、在SpringBoot中的使用

@Async在SpringBoot中的使用还是挺简单的,只需要下面两部操作就可以实现方法的异步调用。

  1. 在启动类上使用@EnableAsync注解
@SpringBootApplication
@EnableAsync
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  1. 将需要异步执行的方法所在的类注入到spring容器,并在异步的方法上添加
@Async注解
@Component
public class AsyncTest {

    /**
 	*  无返回值调用
 	*/
    @Async
    public void asyncDoSomething(){
        // 具体业务逻辑处理
        ……
    }

    /**
 	* 有返回值的调用,使用Future来接收异步任务执行的结果
 	* 对于返回值是Future,需要我们在方法中捕获异常并处理,或者在调用方在调用Futrue.get时捕获异常进行处理
 	* 
 	* @return
 	*/
    @Async
    public Future<String> asyncDoSomethingToReturn() {
        Future<String> future;
        try {
            // 具体业务逻辑处理
            ……
            future = new AsyncResult<String>("success");
        } catch (InterruptedException e) {
            future = new AsyncResult<String>("InterruptedException");
        } catch(IllegalArgumentException e){
            future = new AsyncResult<String>("IllegalArgumentException");
        }
        return future;
    }
}

四、@Async失效场景

  1. 非公有方法:注解标注方法修饰符为非public时,异步任务会失效。因为@Async是基于动态代理实现的,如果被@Async标注的方法的修饰符不是public的话,将不会对该bean进行代理对象创建或者不会对方法进行代理调用,从而也就无法将任务交由现场池中的某个线程去异步执行任务。
  2. final 修饰或者static修饰的方法:如果动态代理使用的是cglib实现的话,由于cglib实现动态代理的方式主要是靠运行期动态创建目标类的子类,从而无法创建代理对象,也就无法将任务交由现场池中的某个线程去异步执行任务。
  3. 类内部访问:在同一个类中调用被@Async方法调用时,也会因为在当前类中无法生成的代理对象来调用,只能调用实际的目标方法,也就无法将任务交由现场池中的某个线程去异步执行任务。
  4. spring无法扫描到异步类,没加注解@Async 或 @EnableAsync注解
  5. 注解@Async的返回值只能为void或者Future
  6. 在Async 方法上标注@Transactional是没用的,但在Async 方法调用的方法上标注@Transactional 是有效的

五、@Async 底层源码解析

5.1 @Async注解源码

image.png 从上面@Async注解源码可以看出,只有一个value属性,这个属性就是线程池Bean的名称,如果在注解中设置了这个值,则根据这个值从Spring容器中获取这个线程池的Bean去异步执行方法;如果没设置,则会使用默认的线程池去异步执行该方法。所以这里想要指定具体的线程池去异步执行方法的话,只需要在@Async(“taskExecutor”)中设置线程池的bean的名称当作注解的value属性值就好。

5.2 @EnableAsync注解源码

image.png (1)根据上面@EnableAsync源码可以看出,该注解中使用了一个@Import注解导入了一个AsyncConfigurationSelector.class类。 image.png (2)进入到AsyncConfigurationSelector类,可以看到该类继承了AdviceModeImportSelector抽象类,AdviceModeImportSelector抽象类又实现了ImportSelector接口。并且在selectImports()方法中根据不同的AdviceMode来选择和返回适当的导入语句。 image.png (3)进入到ProxyAsyncConfiguration配置类中,可以看到会创建一个AsyncAnnotationBeanPostProcessor对象,并设置相关的属性后返回该对象。 image.png (4)再进入到AsyncAnnotationBeanPostProcessor类中,有个setBeanFactory()方法,在这个方法中往BeanFactory容器中增加了一个AsyncAnnotationAdvisor增强器。 image.png (5)进入这个AsyncAnnotationAdvisor增强器的构造函数可以看到,分别创建了一个通知以及切入点。 image.png (6)在这个创建增强方法的的函数中,可以看到创建了一个拦截器,主要通过这个拦截器来拦截标注有@Async注解的方法。 image.png (7)进入到AnnotationAsyncExecutionInterceptor拦截器,可以看到其继承了AsyncExecutionInterceptor,主要的拦截方法就在这个AsyncExecutionInterceptor父类的invoke()方法中。主要步骤也就下面三步:

  1. 获取线程池;
  2. 创建Callbale任务;
  3. 提交任务到线程去执行。 image.png (8)看下获取线程池的处理逻辑。主要步骤是: 1.先从缓存中去查找,如果缓存中存在线程池就直接返回这个线程池去使用; 2.从@Async注解设置了value值,则把value的值当做线程池的bean名称去Spring容器中找到这个bean并返回去使用; 3.如果方法的@Async注解没有设置了value值,则使用默认的线程池去调用。 image.png

(9)看下获取注解值的方法。主要逻辑就是先拿到方法的Async注解,再根据拿到的注解获取该注解的value属性值。 image.png (10)再看下使用默认线程池的方法。主要是先调用父类的getDefaultExecutor方法去获取线程池对象,如果能获取到就直接使用该线程池对象,如果获取不到就新建个SimpleAsyncTaskExecutor线程池对象。 image.png image.png 进入SimpleAsyncTaskExecutor的execute方法去创建线程执行任务。 image.png 在doExecute()方法中每次都会创建一个线程去执行。 image.png

由于SimpleAsyncTaskExecutor是个简单的线程池,通过这个线程池创建的线程不能重复使用,且每次调用都会去创建一个线程,在高并发或者访问很频繁的场景并不适用,很容易造成系统线程资源耗尽,严重可能导致系统崩溃。所以在使用@Async注解去实现方法的异步调用时最好使用自定义线程池或者ThreadPoolTaskExecutor线程池去替换掉@Async注解默认的线程池。

六、注意事项

不建议直接使用@Async,上面源码中已经分析了,如果直接使用,一般会使用到Spring创建的SimpleAsyncTaskExecutor的线程池,每次执行都会创建线程,高并发或者访问很频繁的场景,很容易造成系统线程资源耗尽。所以在调用时,使用自定义线程池。

@Configuration
@EnableAsync
public class AsyncExecutorConfig {

    @Bean("successExecutor")
    public Executor executor() {
        // hutool工具
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNamePrefix("registerSuccessExecutor-").build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 200, 0L, TimeUnit.MICROSECONDS,
                new LinkedBlockingDeque<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy());
        return threadPoolExecutor;
    }
}


在使用@Async使用自定义线程池

@Service
public class AsyncTest {

  
    @Async("successExecutor")
    public void asyncDoSomething() {
        // 具体业务逻辑处理
        ……
    }
  }