你好呀,我是 why。

我之前写过一些关于线程池的文章,然后有同学去翻了一圈,发现我没有写过一篇关于 @Async 注解的文章,于是他来问我:

我习惯用自定义线程池的方式去做一些异步的逻辑,且这么多年一直都是这样用的。

所以如果是我主导的项目,你在项目里面肯定是看不到 @Async 注解的。

那我之前见过 @Async 注解吗?

肯定是见过啊,有的朋友就喜欢用这个注解。

一个注解就搞定异步开发,多爽啊。

我不知道用这个注解的人知不知道其原理,反正我是不知道的。

最近开发的时候引入了一个组件,发现调用的方法里面,有的地方用到了这个注解。

既然这次用到了,那就研究一下吧。

首先需要说明的是,本文并不会写线程池相关的知识点。

仅描述我是通过什么方式,去了解这个我之前一无所知的注解的。

搞个 Demo

不知道大家如果碰到这种情况会去怎么下手啊。

但是我认为不论是从什么角度去下手的,最后一定是会落到源码里面的。

所以,我一般是先搞个 Demo。

Demo 非常简单啊,就三个类。

首先是启动类,这没啥说的:

这个 service 里面的 syncSay 方法被打上了 @Async 注解。

最后,搞个 Controller 来调用它,完事:

我去,从线程名称来看,这也没异步呀?

怎么还是 tomcat 的线程呢?

于是,我就遇到了研究路上的第一个问题:@Async 注解没有生效。

为啥不生效?

为什么不生效呢?

我也是懵逼的,我说了之前对这个注解一无所知,那我怎么知道呢?

那遇到这个问题的时候会怎么办?

当然是面向浏览器编程啦!

这个地方,如果我自己从源码里面去分析为啥没生效,一定也能查出原因。

但是,如果我面向浏览器编程,只需要 30 秒,我就能查到这两个信息:

失效原因:

  • 1.@SpringBootApplication 启动类当中没有添加 @EnableAsync 注解。

  • 2.没有走 Spring 的代理类。因为 @Transactional@Async 注解的实现都是基于 Spring 的 AOP,而 AOP 的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过 Spring 容器管理。

很显然,我这个情况符合第一种情况,没有添加 @EnableAsync 注解。

另外一个原因,我也很感兴趣,但是现在我的首要任务是把 Demo 搭建好,所以不能被其他信息给诱惑了。

很多同学带着问题去查询的时候,本来查的问题是@Async 注解为什么没有生效,结果慢慢的就走偏了,十五分钟后问题就逐渐演变为了 SpringBoot 的启动流程。

再过半小时,网页上就显示的是一些面试必背八股文之类的东西…

我说这个意思就是,查问题就好好查问题。查问题的过程中肯定会由这个问题引发的自己更加感兴趣的问题。但是,记录下来,先不要让问题发散。

这个道理,就和带着问题去看源码一样,看着看着,可能连自己的问题是什么都不知道了。

再次发起调用:

于是,我把程序稍微改造了一下:

结果…

它竟然…

照单全收了,没有异常?

日志一秒打几行,打的很欢乐:

朋友们,你说这是啥意思?

是不是就是说这个我正在寻找的线程池的核心线程数的配置是 8 ?

什么,你问我为什么不能是最大线程数?

有可能吗?

当然有可能。但是我 10000 个任务发过来,没有触发线程池拒绝策略,刚好把最大线程池给用完了?

也就是说这个线程池的配置是队列长度 9992,最大线程数 8 ?

这也太巧合了且不合理了吧?

所以我觉得核心线程数配置是 8 ,队列长度应该是 Integer.MAX_VALUE

为了证实我的猜想,我把请求改成了这样:

那叫一个飙升啊,点击【执行 GC】按钮也没有任何缓解。

也从侧面证明了:任务有可能都进队列里面排队了,导致内存飙升。

虽然,我现在还不知道它的配置是什么,但是经过刚刚的黑盒测试,我有正当的理由怀疑:

默认的线程池有导致内存溢出的风险。

点进这个注解之后,几段英文,不长,我从里面获取到了一个关键信息:

constrained,受限制,被约束的意思。

这句话是说:返回类型被限制为 void 或者 Future。

啥意思呢?

那我偏要返回一个 String 呢?

那这里如果我返回一个对象,岂不是很容易爆出空指针异常?

看完注解上的注释之后,我发现了第二个隐藏的坑:

如果被 @Async 注解修饰的方法,返回值只能是 void 或者 Future。

void 就不说了,说说这个 Future。

看我划线的另外一句:

it will have to return a temporary {@code Future} handle that just passes a value through: e.g. Spring’s {@link AsyncResult}

上有一个 temporary,是四级词汇啊,应该认识的,就是短暂的、暂时的意思。

temporary worker,临时工,明白吧。

所以意思就是如果你要返回值,你就用 AsyncResult 对象来包一下,这个 AsyncResult 就是 temporary worker。

就像这样:

这个注解,看注释上面的意思,就是说这个应该填一个线程池的 bean 名称,相当于指定线程池的意思。

也不知道理解的对不对,等会写个方法验证一下就知道了。

好了,到现在,我把信息整理汇总一下。

  • 我之前完全不懂这个注解,现在我有一个 Demo 了,搭建 Demo 的时候我发现除了 @Async 注解之外,还需要加上 @EnableAsync 注解,比如加在启动类上。

  • 然后把这个默认的线程池当做黑盒测试了一把,我怀疑它的核心线程数默认是 8,队列长度无线长。有内存溢出的风险。

  • 通过阅读 @Async 上的注解,我发现返回值只能是 void 或者 Future 类型,否则即使返回了其他值,不会报错,但是返回的值是 null,有空指针风险。

  • @Async 注解中有一个 value 属性,看注释应该是可以指定自定义线程池的。

接下来我把要去探索的问题排个序,只聚焦到 @Async 的相关问题上:

  • 1.默认线程池的具体配置是什么?

  • 2.源码是怎么做到只支持 void 和 Future 的?

  • 3.value 属性是干什么用的?

具体配置是啥?

我找到具体配置其实是一个很快的过程。

因为这个类的 value 参数简直太友好了:

顺着断点往下调试,就会来到这个地方:

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor

所以,我要找的东西,就是编号为 ② 的这个地方的逻辑。

这里面主要是一个 defaultExecutor 对象:

最终你会调试到这个地方来:

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#getDefaultExecutor

这不就找到我想要的东西了吗,这个线程池的相关参数都可以看到了。

也证实了我之前猜想:

我觉得核心线程数配置是 8 ,队列长度应该是 Integer.MAX_VALUE。

但是,现在我是直接从 BeanFactory 获取到了这个线程池的 Bean,那么这个 Bean 是什么时候注入的呢?

朋友们,这还不简单吗?

我都已经拿到这个 Bean 的 beanName 了,就是 applicationTaskExecutor,但凡你把 Spring 获取 bean 的流程的八股文背的熟练一点,你都知道在这个地方打上断点,加上调试条件,慢慢去 Debug 就知道了:

org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String)

都找到这个类了,随便打个断点,就可以开始调试了。

再说一个骚一点的操作。

假设我现在连 beaName 都不知道,但是我知道它肯定是一个被 Spring 管理的线程池。

那么我就获取项目里面所有被 Spring 管理的线程池,总有一个得是我要找的吧?

你看下面截图,当前这个 bean 不就是我要找的 applicationTaskExecutor 吗?

返回类型的支持

前面我们卷完了第一个关于配置的问题。

接下来,我们看另外一个前面提出的问题:

源码是怎么做到只支持 void 和 Future 的?

答案就藏在这个方法里面:

org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke

其实这里就是我要找的答案。

你看这个方法的入参 returnType 是 String,其实就是被 @Async 注解修饰的 asyncSay 方法。

你要不信,我可以带你看看前一个调用栈,这里可以看到具体的方法:

而我们的程序走到了最后的一个 else,含义就是返回值不是 Future 类型的。

那么你看它干了啥事儿?

用 execute 的方式提交,没有返回值:

当它走到这个方法的时候,返回值已经明确是 null 了。

为什么还用 executor.submit(task) 提交任务呢?

用 execute 就行了啊。

区别,你问我区别?

不是刚刚才说了吗, submit 方法是有返回值的。

接着,再说一下我们前面按下不表的部分,这里编号为 ② 的地方封装的到底是什么?

只是我单独拧出来说的原因是我要给你证明,这里返回的 result 就是我们方法返回的真实的值。

只是判断了一下类型不是 Future 的话就不做处理,比如我这里其实是返回了 hi:1 字符串的,只是不符合条件,就被扔掉了:

甚至修改方法都给你标出来了,你只需要一点,它就给你重新改好了。

@Async 注解的 value

接下来我们看看 @Async 注解的 value 属性是干什么的。

其实在前面我已经悄悄的提到了,只是一句话就带过了,就是这个地方:

再次跑起来,跑到这个断点的地方,就和我们默认的情况不一样了,这个时候 qualifier 有值了:

这个其实是一个很简单的探索过程,但是这背后蕴涵了一个道理。

就是之前有同学问我的这个问题:

然后,还记得我们前面提到的那个维护方法和线程池的映射关系的 map 吗?

就是它:

看明白了吗?

再次复述一次这句话:

以方法维度维护方法和线程池之间的关系。

现在,我对于 @Async 这个注解算是有了一点点的了解,我觉得它也还是很可爱的。后面也许我会考虑在项目里面把它给用起来。毕竟它更加符合 SpringBoot 的基于注解开发的编程理念。

本文文字及图片出自 InfoQ

余下全文(1/3)
分享这篇文章:

请关注我们:

发表评论

邮箱地址不会被公开。 必填项已用*标注