一、先写个Demo
我们直接使用 SpringBoot 搭建个 Demo,首先就是启动类,加入 @EnableAsync 注解。
这就是个别同学使用 @Async 注解不生效的原因,没有在启动类中打开异步的开关。
再写一个Service,定义一个异步方法async。
@Service
public class TestService {
public final Logger log = LoggerFactory.getLogger(getClass());
@Async
public void async(){
log.info("异步线程消息输出:{}",Thread.currentThread().getName());
}
}
注意:异步方法所在的类需要被 spring 管理。
定义调用类,上面说了,需要被 spring 管理,所以调用的时候也需要使用注入的方式进行调用,如果使用 new 或者本类方法调用都是不能生效的。
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
@GetMapping("/async")
public Object async() throws Exception {
testService.async();
return "success";
}
}
启动,调用一下看下输出。
[task-1]c.z.e.encry.service.TestService:异步线程消息输出:task-1
可以看到,打印该日志的线程已经是另一个线程,且线程名task-1。那么是不是可以猜测一下使用的线程池中线程名称前缀为task-,这里先提一嘴,后面我们来揭秘。
到了这,我们的 demo 就搭建完成了,下面开始吃源码吧,源码之下无秘密,Debug 启动。
先看一下 @Async 工作流程图帮助理解。
二、@Async 注解原理
第一步,找到 @Async注解。
注解的内部很简单,就一个 value 属性,这个属性我们后面再说,我先分享一下平常我是如何看源码的。
- 首先我会看注释,尤其是注释中可以点进去的类,比如 See Also中的我会重点关注。
- 其次是看注解的参数调用的地方。
通过上面两步,发现 value 参数调用的类正好与上面标注的 AnnotationAsyncExecutionInterceptor 吻合,所以直接跳到代码调用的位置。
其实在这里我们就可以直接断点,然后根据调用栈就知道在哪调用的了。
打个断点,启动程序,http 调用异步方法 async。
需要注意,如果你断点进不来,那就重启,在应用程序启动之后的第一次访问中会被拦住。(原因后面说,在第五节自定义线程池)
点击图片中红框起来的位置,发现跳到的代码位置正好与 See Also中标注的是同一个方法AsyncExecutionAspectSupport#determineAsyncExecutor ,说明我们没有找错地方,那么我们就开始在这个位置,再加入一个断点,开始我们的 debug 。
AsyncExecutionAspectSupport#determineAsyncExecutor 方法中,其实就是 value 参数生效的地方。
- 80 行代码处,获取注解 @Async 的 value 值。
- 83 行代码处,如果有设置的 value 值,去 spring 容器中获取对应的执行器,对于我们这就是获取对应的线程池。
- 85 行代码处,如果没有设置 value 值,就返回默认的 defaultExecutor 。
继续断点处往下走,所以 AsyncExecutionAspectSupport#determineAsyncExecutor 方法就是返回执行任务的线程池。
- 如果为空抛出异常(代码40行)。
- 否则就封装我们的异步方法 async 为 Callable。
为什么封装为一个 Callable 可以评论区聊一下,看看八股文忘了没有。
继续往下,就到了代码 56 行的位置,提交给线程池执行任务。
所以到了这,你看明白了吗?其实 @Async 注解的核心代码就是 AsyncExecutionInterceptor#invoke() 方法,只要这个方法主线找到了,逻辑通了,那么@ Async 注解原理还不是手到擒来。
三、底层是不是使用的线程池
回到这个问题上来,底层是不是使用的线程池相信你已经有了答案了吧,在 springboot 中,起码是使用的线程池。
当 value 属性值为空时,spring会使用 SimpleAsyncTaskExecutor 执行任务,而该类都是通过 new Thread() 执行任务的,具体可查看 SimpleAsyncTaskExecutor#doExecute(Runnable task) 。
在 AsyncExecutionInterceptor 类中,重写了父类的 getDefaultExecutor ,当我们没有指定 value 参数时,就会走到该方法,返回一个applicationTaskExecutor的线程池。
细心的同学应该看到了吧,此处线程的前缀就是task-,这不就对应我们文章开头日志输出的线程名称了吗。
四、线程池的配置
那么这个线程池是在哪里初始化的呢?我们也没有看到初始化的代码啊?
下面我分享一个找配置的方法,现在我们 beanName 已经知道了,直接全局搜索一下不就好了。
这个还算比较顺利,全局一查找,就一个,不就是你吗。代码位置(TaskExecutionAutoConfiguration#applicationTaskExecutor)
代码中就一行,执行的 build ,所以我们直接进入。
先看一下 configure,简简单单的一波 set 的操作。
所以知道线程池的配置在哪了吗,那肯定就是 new ThreadPoolTaskExecutor 这了。
在 configure 处打一个断点,即可看到全部的配置信息,包括线程名称前缀的指定都在这了。
五、可以自定义线程池吗
那么可以自定义线程池吗,当然可以。还记得文章开头我们提到的 @Async 注解中的 value 属性吗,它的值就是指定线程池名称的。
我们通过一个代码示例来看下是如何使用自定义线程池的。
上文中我们自定义的线程池,把线程名的前缀改为了zuiyuThreadPool-,如果生效,日志将会打印出线程名称。
需要注意的就两点:
- @Bean 中自定义线程池的注册bean名称
- @Async中指定线程池的名称,保证与第一步的名称保持一致。
启动程序,debug 开始。还记得刚开始我们查找 @Async 注解中value参数使用的地方吗?这个地方就是获取我们注解中值的位置。
在 determineAsyncExecutor 方法处,第 80 行 this.getExecutorQualifier(method) 就是获取注解中值的代码,此处返回了zuiyuThreadPool。实现是 AnnotationAsyncExecutionInterceptor#getExecutorQualifier。
然后 this.findQualifiedExecutor(this.beanFactory, qualifier) 这一行代码中,只做了一件事,就是拿着 value 值去 beanFactory 中查找对应的 bean 对象返回。
上面是首次调用的时候的逻辑,注意看上图的 78 行,所以当你第二次调用的时候就不会走这个逻辑了。
那么这个 executors 是什么呢?点进去看一下。
在此处打个断点看一下,executors 其实就是一个 map,key 就是注解标注的方法,value 就是该方法对应的线程池。
这就是为什么上文中只有程序第一次启动的时候才会进入到获取注解属性值的方法。
六、返回值怎么获取
如果想获取接口的返回值有什么方法吗?
在@Async 注释的地方,返回类型只能是 void 或者java.util.concurrent.Future。
所以我们只需要把异步的方法,改为这两种形式的返回值即可。
假如我们想返回 String 类型的值,可以这样做。
如上图,我们把异步方法改为 CompletableFuture
其实归根结底还是线程池,你还记得 AsyncExecutionInterceptor#invoke 方法吗,最后一行代码不就是 submit 提交任务。
我们把异步的方法封装为了一个 Callable task,然后提交。
而在 doSubmit 方法中,校验方法返回值类型是不是Future类型,如果不是直接提交任务,返回 null。
所以,知道为什么在异步方法中需要封装为 Future 了吧,如果不封装为Future类型,返回为 null,是获取不到结果的。
总结
@Async 注解的工作原理就是文章开头给出流程图所示的流程。核心代码就是AsyncExecutionInterceptor#invoke() ,重点关注 determineAsyncExecutor() 与doSubmit()即可。
大致流程如下:
- 从缓存 map 中获取线程池实例。
- 如果缓存中存在,直接返回。
- 如果缓存中不存在,判断是否指定 value值。
- 如果指定 value 值,就去 beanFactory 中获取对应的线程池实例。
- 如果没有指定,value 为空,就获取 taskExecutor的实例。
- 返回线程池实例。
在第5步中,springboot 中如果获取 taskExecutor 实例时,因为引入了第三方的jar,获取到了第三方的线程池,可能会遇到意想不到的 bug,这个点是需要注意的。