我们都知道@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调试,会进入另一个类
org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke
其中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)也就迎刃而解了。
现在再回来看(疑问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的值,那么此时也会使用我们自定义的配置。
那么为什么是这样的呢?
参考:https://www.cnblogs.com/thisiswhy/p/15233243.html
标签:执行器,import,配置,springframework,线程,org,Async,多少,知道 From: https://www.cnblogs.com/zys2019/p/18438975