关于java:别问了我真的不喜欢这个注解

3次阅读

共计 7670 个字符,预计需要花费 20 分钟才能阅读完成。

你好呀,我是 why。

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

是的,我摊牌了。

我不喜爱这个注解的起因,是因为我压根就没用过。

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

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

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

必定是见过啊,有的敌人就喜爱用这个注解。

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

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

最近开发的时候引入了一个组件,发现调用的办法外面,有的中央用到了这个注解。

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

首先须要阐明的是,本文并不会写线程池相干的知识点。

仅形容我是通过什么形式,去理解这个我之前无所不知的注解的。

搞个 Demo

不晓得大家如果碰到这种状况会去怎么下手啊。

然而我认为不论是从什么角度去下手的,最初肯定是会落到源码外面的。

所以,我个别是先搞个 Demo。

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

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

而后搞个 service:

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

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

Demo 就搭建好了,你也入手去搞一个,耗时超过 5 分钟,算我输。

而后,把我的项目启动起来,调用接口,查看日志:

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

怎么还是 tomcat 的线程呢?

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

为啥不失效?

为什么不失效呢?

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

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

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

这个中央,如果我本人从源码外面去剖析为啥没失效,肯定也能查出起因。

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

生效起因:

  • 1.@SpringBootApplication 启动类当中没有增加 @EnableAsync 注解。
  • 2. 没有走 Spring 的代理类。因为 @Transactional@Async 注解的实现都是基于 Spring 的 AOP,而 AOP 的实现是基于动静代理模式实现的。那么注解生效的起因就很显著了,有可能因为调用办法的是对象自身而不是代理对象,因为没有通过 Spring 容器治理。

很显然,我这个状况合乎第一种状况,没有增加 @EnableAsync 注解。

另外一个起因,我也很感兴趣,然而当初我的首要任务是把 Demo 搭建好,所以不能被其余信息给引诱了。

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

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

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

这个情理,就和带着问题去看源码一样,看着看着,可能连本人的问题是什么都不晓得了。

好了,说回来。

我在启动类上加上该注解:

再次发动调用:

能够看到线程名字变了,阐明真的就好了。

当初我的 Demo 曾经搭好了,能够开始找角度去卷了。

从下面的日志我也能晓得,在默认状况下有一个线程前缀为 task- 的线程池在帮我执行工作。

说到线程池,我就得晓得这个线程池的相干配置才释怀。

那么我怎么能力晓得呢?

先压一压

其实正常人的思路这个时候就应该是去翻源码,找对应的注入线程池的中央。

而我,就有点不失常了,我懒得去源码外面找,我想让它本人裸露到我的背后。

怎么让它裸露进去呢?

仗着我对线程池的理解,我的第一个思路是先压一压这个线程池。

压爆它,压的它解决不过去工作,让它走到回绝逻辑外面去,失常来说是会抛出异样的吧?

于是,我把程序略微革新了一下:

想的是间接来一波鼎力出奇观:

后果 …

它居然 …

照单全收了,没有异样?

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

尽管没有呈现我料想的回绝异样,然而我从日志外面还是看出了一点点端倪。

比方我就发现这个 taks 最多就到 8:

敌人们,你说这是啥意思?

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

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

有可能吗?

当然有可能。然而我 10000 个工作发过来,没有触发线程池回绝策略,刚好把最大线程池给用完了?

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

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

所以我感觉外围线程数配置是 8,队列长度应该是 Integer.MAX_VALUE

为了证实我的猜测,我把申请改成了这样:

num= 一千万。

通过 jconsole 察看堆内存应用状况:

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

也从侧面证实了:工作有可能都进队列外面排队了,导致内存飙升。

尽管,我当初还不晓得它的配置是什么,然而通过刚刚的黑盒测试,我有正当的理由狐疑:

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

然而,同时也意味着我想从让它抛出异样,从而本人裸露在我背后的骚想法落空。

怼源码

后面的思路走不通,老老实实的开始怼源码吧。

我是从这个注解开始怼的:

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

次要关注我画线的中央。

In terms of target method signatures, any parameter types are supported.

在指标办法的签名中,入参是任何类型都反对的。

多说一句:这里说到指标办法,说到 target,大家脑海外面应该是要立即呈现一个代理对象的概念的。

下面这句话好了解,甚至感觉是一句废话。

然而,它紧跟了一个 However:

However, the return type is constrained to either void or Future.

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

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

啥意思呢?

那我偏要返回一个 String 呢?

WTF,打印进去的竟然是 null!?

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

看完注解上的正文之后,我发现了第二个暗藏的坑:

如果被 @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。

就像这样:

接着咱们把眼光放到注解的 value 属性上:

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

也不晓得了解的对不对,等会写个办法验证一下就晓得了。

好了,到当初,我把信息整顿汇总一下。

  • 我之前齐全不懂这个注解,当初我有一个 Demo 了,搭建 Demo 的时候我发现除了 @Async 注解之外,还须要加上 @EnableAsync 注解,比方加在启动类上。
  • 而后把这个默认的线程池当做黑盒测试了一把,我狐疑它的外围线程数默认是 8,队列长度无线长。有内存溢出的危险。
  • 通过浏览 @Async 上的注解,我发现返回值只能是 void 或者 Future 类型,否则即便返回了其余值,不会报错,然而返回的值是 null,有空指针危险。
  • @Async 注解中有一个 value 属性,看正文应该是能够指定自定义线程池的。

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

  • 1. 默认线程池的具体配置是什么?
  • 2. 源码是怎么做到只反对 void 和 Future 的?
  • 3.value 属性是干什么用的?

具体配置是啥?

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

因为这个类的 value 参数几乎太敌对了:

五处调用的中央,其中到处都是正文。

无效的调用就这一个中央,间接先打上断点再说:

org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor#getExecutorQualifier

发动调用之后,果然跑到了断点这个中央:

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

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor

这个代码构造十分的清晰。

编号为 ① 的中央,是获取对应办法上的 @Async 注解的 value 值。这个值其实就是 bean 名称,如果不为空则从 Spring 容器中获取对应的 bean。

如果 value 是没有值的,也就是咱们 Demo 的这种状况,会走到编号为 ② 的中央。

这个中央就是我要找的默认的线程池。

最初,不论是默认的线程池还是 Spring 容器中咱们自定义的线程池。

都会以办法为维度,在 map 中保护办法和线程池的映射关系

也就是编号为 ③ 的这一步,代码中的 executors 就是一个 map:

所以,我要找的货色,就是编号为 ② 的这个中央的逻辑。

这外面次要是一个 defaultExecutor 对象:

这个玩意是一个函数式编程,所以如果你不晓得这个玩意是干什么的,调试起来可能有点懵逼:

我倡议你去恶补一下,10 分钟就能入门。

最终你会调试到这个中央来:

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#getDefaultExecutor

这个代码就有点意思了,就是从 BeanFactory 外面获取一个默认的线程池相干的 Bean 进去。流程很简略,日志也打印的很分明,就不赘述了。

然而我想说的有意思的点是,我不晓得你看到这份代码,有没有看出一丝丝双亲委派内味。

都是利用异样,在异样外面解决逻辑。

就下面这“垃圾”代码,间接就触犯了阿里开发标准中的两大条:

在源码外面这就是好代码。

在业务流程外面,这就是违反了标准。

所以,说一句题外话。

就是阿里开发标准我个人感觉,其实是针对咱们写业务代码的共事一个最佳实际。

然而当把这个尺度拉到中间件、根底组件、框架源码的范畴时,就会呈现一点水土不服的症状,这个货色见仁见智,我是感觉阿里开发标准的 idea 插件,对于我这样写增删查改的程序员来说,是真的香。

不说远了,咱们还是回来看看获取到的这个线程池:

这不就找到我想要的货色了吗,这个线程池的相干参数都能够看到了。

也证实了我之前猜测:

我感觉外围线程数配置是 8,队列长度应该是 Integer.MAX_VALUE。

然而,当初我是间接从 BeanFactory 获取到了这个线程池的 Bean,那么这个 Bean 是什么时候注入的呢?

敌人们,这还不简略吗?

我都曾经拿到这个 Bean 的 beanName 了,就是 applicationTaskExecutor,凡是你把 Spring 获取 bean 的流程的八股文背的纯熟一点,你都晓得在这个中央打上断点,加上调试条件,缓缓去 Debug 就晓得了:

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

假如你就是不晓得在下面这个中央打断点去调试呢?

再说一个简略粗犷的办法,你都拿到 beanName 了,在代码外面一搜不就进去了嘛。

简略粗犷成果好:

org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration

都找到这个类了,轻易打个断点,就能够开始调试了。

再说一个骚一点的操作。

假如我当初连 beaName 都不晓得,然而我晓得它必定是一个被 Spring 治理的线程池。

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

你看上面截图,以后这个 bean 不就是我要找的 applicationTaskExecutor 吗?

这都是一些野路子,骚操作,晓得就好,有时候多个排查思路。

返回类型的反对

后面咱们卷完了第一个对于配置的问题。

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

源码是怎么做到只反对 void 和 Future 的?

答案就藏在这个办法外面:

org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke

标号为 ① 的中央,其实就是咱们后面剖析的从 map 外面拿 method 对应的线程池的办法。

拿到线程池之后来到标号为 ② 的中央,就是封装一个 Callable 对象。

那么是把什么封装到 Callable 对象外面呢?

这个问题先按下不表,咱们先牢牢的围绕咱们的问题往下走,不然问题会越来越多。

标号为 ③ 的中央,doSubmit,见名知意,这个中央就是执行工作的中央了。

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#doSubmit

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

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

你要不信,我能够带你看看前一个调用栈,这里能够看到具体的办法:

怎么样,没有骗你吧。

所以,当初你再看 doSubmit 办法拿着这个办法的返回类型干啥了。

一共四个分支,后面三个都是判断是否是 Future 类型的。

其中的 ListenableFuture 和 CompletableFuture 都是继承自 Future 的。

这个两个类在 @Async 注解的办法正文外面也提到了:

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

那么你看它干了啥事儿?

间接把工作 submit 到线程池之后,就返回了一个 null。

这可不得爆出空指针异样吗?

到这个中央,咱们也解决了这个问题:

源码是怎么做到只反对 void 和 Future 的?

其实情理很简略,咱们失常的应用线程池提交不也就这两个返回类型吗?

用 submit 的形式提交,返回一个 Future,把后果封装到 Future 外面:

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

而框架通过一个简略的注解帮咱们实现异步化,它玩的再花里胡哨,就算是玩出花来了,它也得恪守线程池提交的底层原理啊。

所以,源码为什么只反对 void 和 Future 的返回类型?

因为底层的线程池只反对这两种类型的返回。

只是它的做法略微有点坑,间接把其余的返回类型的返回值都解决为 null 了。

你还别不服,谁叫你不读正文上的阐明呀。

另外,我发现这个中央还有个小的优化点:

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

为什么还用 executor.submit(task) 提交工作呢?

用 execute 就行了啊。

区别,你问我区别?

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

尽管你不必,然而它还是会去构建一个返回的 Future 对象呀。

然而构建进去了,也没用上呀。

所以间接用 execute 提交就行了。

少生成一个 Future 对象,算不算优化?

有一说一,不算什么有价值的优化,然而说进来可是优化过 Spring 的源码的,装逼够用了。

接着,再说一下咱们后面按下不表的局部,这里编号为 ② 的中央封装的到底是什么?

其实这个问题用脚指头应该也猜到了:

只是我独自拧进去说的起因是我要给你证实,这里返回的 result 就是咱们办法返回的实在的值。

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

另外,idea 还是很智能的,它会提醒你这个中央的返回值是有问题的:

甚至批改办法都给你标出来了,你只须要一点,它就给你从新改好了。

对于为什么要这么改,当初咱们曾经拿捏的十分分明了。

知其然,也知其所以然。

@Async 注解的 value

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

其实在后面我曾经悄悄的提到了,只是一句话就带过了,就是这个中央:

后面说编号为 ① 的中央,是获取对应办法上的 @Async 注解的 value 值。这个值其实就是 bean 名称,如果不为空则从 Spring 容器中获取对应的 bean。

而后我就间接剖析到标号为 ② 的中央了。

当初咱们从新看看标号为 ① 的中央。

我也重新安排一个测试用例去验证我的想法。

反正 value 值应该是 Spring 的 bean 名称,而且这个 bean 肯定是一个线程池对象,这个没啥说的。

所以,我把 Demo 程序修改为这样:

再次跑起来,跑到这个断点的中央,就和咱们默认的状况不一样了,这个时候 qualifier 有值了:

接下来就是去 beanFactory 外面拿名字为 whyThreadPool 的 bean 了。

最初,拿进去的线程池就是我自定义的这个线程池:

这个其实是一个很简略的摸索过程,然而这背地蕴涵了一个情理。

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

其实这个问题挺有代表性的,很多同学都认为线程池不能滥用,一个我的项目共用一个就好了。

线程池的确不能滥用,然而一个我的项目外面的确是能够有多个自定义线程池的。

依据你的业务场景来划分。

比方举个简略的例子,业务主流程上能够用一个线程池,然而当主流程中的某个环节出问题了,假如须要发送预警短信。

发送预警短信的这个操作,就能够用另外一个线程池来做。

它们能够共用一个线程池吗?

能够,能用。

然而会呈现什么问题呢?

假如我的项目中某个业务出问题了,在一直的,疯狂的发送预警短信,甚至把线程池都占满了。

这个时候如果主流程的业务和发送短信用的是同一个线程池,会呈现什么漂亮的场景?

是不是一提交工作,就间接走到回绝策略外面去了?

预警短信发送这个从属性能,导致了业务不能够,轻重倒置的了吧?

所以,倡议应用两个不同的线程池,各司其职。

这其实就是听起来很高大上的线程池隔离技术。

那么落到 @Async 注解上是怎么回事呢?

其实就是这样的:

而后,还记得咱们后面提到的那个保护办法和线程池的映射关系的 map 吗?

就是它:

当初,我把程序跑起来调用一下下面的三个办法,目标是为了把值给放进去这个 map:

看明确了吗?

再次复述一次这句话:

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

当初,我对于 @Async 这个注解算是有了一点点的理解,我感觉它也还是很可恶的。前面兴许我会思考在我的项目外面把它给用起来。毕竟它更加合乎 SpringBoot 的基于注解开发的编程理念。

最初说一句

好了,看到了这里了,点赞、关注轻易安顿一个吧,要是你都安顿上我也不介意。写文章很累的,须要一点正反馈。

给各位读者敌人们磕一个了:

正文完
 0