返回类型的支持
前面我们卷完了第一个关于配置的问题。
接下来,我们看另外一个前面提出的问题:
源码是怎么做到只支持 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 里面:
而框架通过一个简单的注解帮我们实现异步化,它玩的再花里胡哨 ,就算是玩出花来了,它也得遵守线程池提交的底层原理啊。
所以,源码为什么只支持 void 和 Future 的返回类型?
因为底层的线程池只支持这两种类型的返回。
只是它的做法稍微有点坑,直接把其他的返回类型的返回值都处理为 null 了。
你还别不服,谁叫你不读注释上的说明呀。
另外,我发现这个地方还有个小的优化点:
当它走到这个方法的时候,返回值已经明确是 null 了。
为什么还用 executor.submit(task)
提交任务呢?
用 execute 就行了啊。
区别,你问我区别?
不是刚刚才说了吗, submit 方法是有返回值的。
虽然你不用,但是它还是会去构建一个返回的 Future 对象呀。
然而构建出来了,也没用上呀。
所以直接用 execute 提交就行了。
少生成一个 Future 对象,算不算优化?
有一说一,不算什么有价值的优化,但是说出去可是优化过 Spring 的源码的,装逼够用了。
接着,再说一下我们前面按下不表的部分,这里编号为 ② 的地方封装的到底是什么?
其实这个问题用脚指头应该也猜到了:
只是我单独拧出来说的原因是我要给你证明,这里返回的 result 就是我们方法返回的真实的值。
只是判断了一下类型不是 Future 的话就不做处理,比如我这里其实是返回了 hi:1
字符串的,只是不符合条件,就被扔掉了:
对于为什么要这么改,现在我们已经拿捏的非常清楚了。
知其然,也知其所以然。
@Async 注解的 value
接下来我们看看 @Async 注解的 value 属性是干什么的。
其实在前面我已经悄悄的提到了,只是一句话就带过了,就是这个地方:
前面说编号为 ① 的地方,是获取对应方法上的 @Async
注解的 value 值。这个值其实就是 bean 名称,如果不为空则从 Spring 容器中获取对应的 bean。
然后我就直接分析到标号为 ② 的地方了。
现在我们重新看看标号为 ① 的地方。
我也重新安排一个测试用例去验证我的想法。
反正 value 值应该是 Spring 的 bean 名称,而且这个 bean 一定是一个线程池对象,这个没啥说的。
所以,我把 Demo 程序修改为这样:
再次跑起来,跑到这个断点的地方,就和我们默认的情况不一样了,这个时候 qualifier 有值了:
接下来就是去 beanFactory 里面拿名字为 whyThreadPool 的 bean 了。
最后,拿出来的线程池就是我自定义的这个线程池:
这个其实是一个很简单的探索过程,但是这背后蕴涵了一个道理。
就是之前有同学问我的这个问题:
其实这个问题挺有代表性的,很多同学都认为线程池不能滥用,一个项目共用一个就好了。
线程池确实不能滥用,但是一个项目里面确实是可以有多个自定义线程池的。
根据你的业务场景来划分。
比如举个简单的例子,业务主流程上可以用一个线程池,但是当主流程中的某个环节出问题了,假设需要发送预警短信。
发送预警短信的这个操作,就可以用另外一个线程池来做。
它们可以共用一个线程池吗?
可以,能用。
但是会出现什么问题呢?
假设项目中某个业务出问题了,在不断的,疯狂的发送预警短信,甚至把线程池都占满了。
这个时候如果主流程的业务和发送短信用的是同一个线程池,会出现什么美丽的场景?
是不是一提交任务,就直接走到拒绝策略里面去了?
预警短信发送这个附属功能,导致了业务不可以,本末倒置的了吧?
所以,建议使用两个不同的线程池,各司其职。
这其实就是听起来很高大上的线程池隔离技术。
那么落到 @Async
注解上是怎么回事呢?
其实就是这样的:
然后,还记得我们前面提到的那个维护方法和线程池的映射关系的 map 吗?
就是它:
看明白了吗?
再次复述一次这句话:
以方法维度维护方法和线程池之间的关系。
现在,我对于 @Async
这个注解算是有了一点点的了解,我觉得它也还是很可爱的。后面也许我会考虑在项目里面把它给用起来。毕竟它更加符合 SpringBoot 的基于注解开发的编程理念。
最后说一句
好了,看到了这里了,点赞、关注随便安排一个吧,要是你都安排上我也不介意。写文章很累的,需要一点正反馈。
给各位读者朋友们磕一个了: