别问了,我真的不喜欢这个注解!
你好呀,我是 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 条讨论