文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

一文搞懂 @Async 注解原理

2024-11-29 20:53

关注

一、先写个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 属性,这个属性我们后面再说,我先分享一下平常我是如何看源码的。

通过上面两步,发现 value 参数调用的类正好与上面标注的 AnnotationAsyncExecutionInterceptor 吻合,所以直接跳到代码调用的位置。

其实在这里我们就可以直接断点,然后根据调用栈就知道在哪调用的了。

打个断点,启动程序,http 调用异步方法 async。

需要注意,如果你断点进不来,那就重启,在应用程序启动之后的第一次访问中会被拦住。(原因后面说,在第五节自定义线程池)

点击图片中红框起来的位置,发现跳到的代码位置正好与 See Also中标注的是同一个方法AsyncExecutionAspectSupport#determineAsyncExecutor ,说明我们没有找错地方,那么我们就开始在这个位置,再加入一个断点,开始我们的 debug 。

AsyncExecutionAspectSupport#determineAsyncExecutor 方法中,其实就是 value 参数生效的地方。

继续断点处往下走,所以 AsyncExecutionAspectSupport#determineAsyncExecutor 方法就是返回执行任务的线程池。

为什么封装为一个 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-,如果生效,日志将会打印出线程名称。

需要注意的就两点:

启动程序,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 的形式就可以返回 String 类型的值了,使用Funture.get() 方法就可以读取到该值。

其实归根结底还是线程池,你还记得 AsyncExecutionInterceptor#invoke 方法吗,最后一行代码不就是 submit 提交任务。

我们把异步的方法封装为了一个 Callable task,然后提交。

而在 doSubmit 方法中,校验方法返回值类型是不是Future类型,如果不是直接提交任务,返回 null。

所以,知道为什么在异步方法中需要封装为 Future 了吧,如果不封装为Future类型,返回为 null,是获取不到结果的。

总结

@Async 注解的工作原理就是文章开头给出流程图所示的流程。核心代码就是AsyncExecutionInterceptor#invoke() ,重点关注 determineAsyncExecutor() 与doSubmit()即可。

大致流程如下:

  1. 从缓存 map 中获取线程池实例。
  2. 如果缓存中存在,直接返回。
  3. 如果缓存中不存在,判断是否指定 value值。
  4. 如果指定 value 值,就去 beanFactory 中获取对应的线程池实例。
  5. 如果没有指定,value 为空,就获取 taskExecutor的实例。
  6. 返回线程池实例。

在第5步中,springboot 中如果获取 taskExecutor 实例时,因为引入了第三方的jar,获取到了第三方的线程池,可能会遇到意想不到的 bug,这个点是需要注意的。

来源:醉鱼Java内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯