@Async你知道多少?

我们都知道 @Async 是一个异步注解,用于在线程池异步执行任务,但是你真的了解其原理吗?

先来一个demo:

1)controller

package com.zxh.controller;


import com.zxh.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/api")
public class TestController{
    @Autowired
    private TestService testService;

    @GetMapping("/test")
    public void  test(){
        for (int i = 0; i < 100; i++) {
            testService.test(i);
        }
    }

}

2)service

package com.zxh.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class TestService {

    @Async
    public void test(Integer i) {
        if (i % 2 == 0) {
            log.info("是偶数");
        } else {
            log.info("是奇数");
        }
    }
}

3)在启动类添加注解,开启异步

 4)启动后访问http://localhost:8081/api/test,打印日志如下:

 那么这里我们几乎是0配置,就能实现异步,且这些线程名称都是以  task-  开头,使用的是默认的线程池配置。如果仔细看日志会发现,这些名称最多直到8。那么是否可以推测出线程池默认的核心线程数是8?那么最大线程数又是多少呢?队列最大长度?(疑问1

我们带着疑问打开这个注解

在注解的注释上有一些关键信息(已通过箭头方式标记)

首先可以看出这个注解可用在类和方法上。用在类上时,等价于在类中的所有方法上添加该注解。

对于①,简单来说就是对于目标方法,入参支持任何类型,但是!其返回值类型只限于 void 和 Future 。既然限制了返回值只有两种,如果我返回String会是什么结果呢?那就来试一下

就此得出结论,如果异步方法的返回值不是void和Future,那么最终的返回值都是null。那么为什么会这样呢,这个疑问稍后在源码中继续寻找答案(疑问2)。

既然返回String时返回值是null,那么我就是需要此类型怎么办呢?那就来到了②,意思就是说如果需要特定的返回值,那么可以使用 AsyncResult  对象封装一下。看到这个是不是一脸懵逼,啥意思?该怎么使用呢?且看改造后的代码

 其实说白了,就是通过AsyncResult 对象封装后返回Future,然后通过get()方法来获取参数。所以说,通过AsyncResult 封装后也是返回的Future类型,如果不按此规则返回,那么返回的值是null,有空指针分险!

 接下来看注解中的属性,只有一个参数value,根据③注释可以知道,就是指定使用哪个执行器来执行异步任务(即指定线程池的名称)。通过value()点进去后发现只有一个地方使用这个值,其他地方都是注释的引用

 方法类路径

org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor#getExecutorQualifier

进入后打断点进行调试(调用时如果没有进入断点则需要重启服务,目前我还不清楚为啥是这样,应该怎么解决?)

 进入这个断点时继续F8进行调试,会进入另一个类

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor

 

执行逻辑是先根据传入的方法获取执行器,由于没有指定执行器,故执行器是null。下面的逻辑就清晰了

根据方法获取@Async的value值,也就是执行器bean的名称(即①)。如果传入的执行器名称不为空,则从spring容器中获取目标执行器(即②),反之则获取默认配置的执行器(即③)。

获取到执行器后,在executors中维护方法和线程池的对应关系,executors是map类型,defaultExecutor是函数式接口Supplier。对于执行器对象信息,后面再细说。

 继续F8调试,会进入另一个类,调用invoke方法

org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke

 这个invoke()方法是否眼熟?不眼熟也没关系,稍后看是如何来的(疑问3)

其中L38是用来用map中根据method获取线程名称,也就是上一步executors。

L42~L54的代码,主要是封装一个Callable 对象,其内部对于return大致一看是不是就分为两种,分别是L46和L54。仔细看其中的逻辑,在try中先获取我们异步方法的返回值类型,如果是Future类型,会返回其值,反之直接reutrn null。你以为到这里就完了?就可以解释为啥限制了void和Future?那你就特错特错了,其实这里的判断是针对任务设置的,听不懂?那没关系。先看下面的doSubmit()方法。

最终把Callable 对象放到doSubmit()方法中,而doSubmit()方法看字面意思是提交任务,实际上也是这么个意思,就是来执行任务的。

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#doSubmit

 

 这里有四个分支,其中 ListenableFuture 和 CompletableFuture 都继承自 Future ,所以前三个分支就是用来判断是否是 Future 类型的。如果不是 Future 类型,则直接返回null。那么对于源码注释中 “返回值限制是void和Future” 的问题(疑问2)也就迎刃而解了。

对于(疑问3),可以看到类AsyncExecutionInterceptor实现了MethodInterceptor接口,从而来重写的invoke()方法

 那么MethodInterceptor又是干嘛用的?MethodInterceptor是一个方法拦截器,用于Spring AOP编程中的动态代理。实现该接口重写invoke方法可对需要增强的方法进行增强。这里的作用就是把使用异步注解的方法提交给线程池执行。

现在再回来看(疑问1),默认配置的核心线程数究竟是多少呢?上面并没有对执行器对象进行说明,这里通过断点可以看出,核心线程数是8,最大线程数 Integer.MAX_VALUE。默认的执行器的bean名称是  applicationTaskExecutor ,线程名称前缀是  task- ,

 那么这个类,在哪里配置的呢?既然已经知道了beanName,那么就简单了,按下两次Shift进行搜索

 找到这个自动配置类后打断点,重启后会自动进入断点

 就是在这里自动注入的默认的配置,可以看出大部分配置都来自properties

org.springframework.boot.autoconfigure.task.TaskExecutionProperties

在这里首先看到了线程名称的前缀配置,其他属性从Pool对象中读取

 自此(疑问1)也解决了。

 既然上述默认值的配置都看完了,那么如何进行自定义配置线程池参数并指定线程池名称呢?这就又回到了 @Async 注解的 value 属性了,下面修改程序如下

也就是自定义了执行器的配置,将其交给Spring管理。在@Async上标注执行器的beanName。重启后打断点

此时会进入②而不是③,因为此时指定了线程池名称,执行器对象如下,已经变成我们自己配置的值。

 那么此时就又出现一个疑问:一个项目共用一个线程池配置按业务使用线程池配置更佳?

其实正常情况下,所有异步的操作共用一个线程池配置是没问题的,但是也不是一概而论。根据实际场景需要,不同的业务最好使用不同的线程池配置,这样不同的业务之间如果队列出现问题不会影响这个服务,降低了耦合性。

上述自定义线程池配置时,是把线程池的配置写在业务类中,在实际开发中是不符合规范的,故需要将其抽取处理,如下

package com.zxh.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;


/**
 * @Auther: zxh
 * @Date: 20240930
 * @Description: 配置线程池
 */
@Configuration
@EnableAsync
public class TaskExecutePool {
    //核心线程数
    private static final Integer CORE_POOL_SIZE = 20;
    //最大线程数
    private static final Integer MAX_POOL_SIZE = 20;
    //缓存队列容量
    private static final Integer QUEUE_CAPACITY = 200;
    //线程活跃时间(秒)
    private static final Integer KEEP_ALIVE = 60;
    //默认线程名称前缀
    private static final String THREAD_NAME_PREFIX = "MyExecutor-";


    @Bean("MyAsyncTaskExecutor")
    public Executor myTaskAsyncPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setKeepAliveSeconds(KEEP_ALIVE);
        executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
        //拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

}

其实我还发现一个很奇怪的线程,只要我自定义了线程池配置并注入到Spring,如果在使用@Async时不写value的值,那么此时也会使用我们自定义的配置。

 那么为什么是这样的呢?那就不得不说执行器的初始化了。

下图中,上面已经说了,如果不配置@Async的value值则会使用默认的执行器(即③)

通过断点可以看出,此时defaultExecutor中配置参数是我们自定义的值

那么defaultExecutor是什么时候初始化?

其实可以看到是在构造方法里面进行赋值的

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#AsyncExecutionAspectSupport

构造方法有两个,那么究竟在哪里调用的?

这就需要注意到 @EnableAsync 注解,只有在启动类或其他配置类添加此注解,异步才会生效,此注解显得尤为重要。那么它是如何做的?

进入注解

org.springframework.scheduling.annotation.EnableAsync

 

可以看出,这个注解引入了一个异步配置类(即①),这个类在 Spring 容器启动时会被调用,用于选择和导入异步相关的配置类。也需要注意它默认的通知方式是 PROXY (即②)。

进入这个配置类

org.springframework.scheduling.annotation.AsyncConfigurationSelector#selectImports

上述默认的通知方式是 PROXY  也就是代理模式

 那么进入这个类

org.springframework.scheduling.annotation.ProxyAsyncConfiguration

 这个类是基于代理实现异步的配置类。

 重点就来看AsyncAnnotationBeanPostProcessor类,这里直接创建了对象并注入。类的dragiam关系图如下,主要看  BeanFactoryAware  和  BeanPostProcessor 这两个接口。

 其中 "BeanFactoryAware" 只有一个方法

org.springframework.beans.factory.BeanFactoryAware#setBeanFactory

这个方法主要就是用来向Spring容器注入我们自己的类。

而 BeanPostProcessor 作用是在bean初始化之前或之后对bean做一些操作

了解之后我们再回到AsyncAnnotationBeanPostProcessor类,可以看到其实现了  setBeanFactory 方法,

org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor#setBeanFactory

 

 这个方法中关键的是L149,创建了一个异步注解通知类  AsyncAnnotationAdvisor ,然后把这个类注入到Spring工厂中

org.springframework.scheduling.annotation.AsyncAnnotationAdvisor

这个类定义通知和切面。而且上述调用的构造方法如下,分别调用了buildAdvice()和buildPointcut()

 这里主要说明buildAdvice()

构造了一个拦截器,再来看看这个拦截器的dragiam关系图

这个类AsyncExecutionAspectSupport是不是似曾相识

最终是调用L92初始化默认执行器。通过断点可以看出在启动时参数defaultExecutor是null,可想而知关键点是getDefaultExecutor()

 由于参数beanFactory是null,故返回值也是null,此时defaultExecutor=null。

而对于@Async有自定义线程池配置但value未设置值也会使用自定义的配置的问题,

官方给出的回答是会根据配置的顺序和规则去查找可用的线程池。如果只有一个自定义的线程池被配置,那么即使没有指定 value,也可能会默认使用这个唯一的自定义线程池。

参考:https://www.cnblogs.com/thisiswhy/p/15233243.html

 

posted @ 2024-09-30 14:13  钟小嘿  阅读(29)  评论(0编辑  收藏  举报