你好,我是why。之前写过一些关于线程池的文章,后来有个同学去转了一圈,发现我没有写过@Async注解的文章,就来问我:对,摊牌。我不喜欢这个注解的原因是因为我根本没有用过它。习惯用自定义线程池做一些异步逻辑,这么多年一直在用。所以如果是我主导的项目,你肯定不会在项目中看到@Async注解。那我以前见过@Async注解吗?肯定是看到了,有些朋友喜欢用这个注解。用一个注解来处理异步开发是多么的酷啊。不知道用这个注解的人知不知道它的原理,反正我也不知道。最近开发的时候,引入了一个组件,发现调用的方法中有些地方使用了这个注解。既然这次用到了,那就研究一下吧。首先要说明的是,本文不会写线程池相关的知识点。就描述一下我是怎么知道这个之前不知道的注解的吧。贴个Demo,不知道大家遇到这种情况会怎么下手。但是我觉得不管从什么角度出发,最后肯定会落入源码。所以,我通常先做一个演示。Demo很简单,就三个类。首先是启动类,没什么好说的:然后创建一个服务:这个服务中的syncSay方法是用@Async注解的。最后弄一个Controller来调用,就大功告成了:Demo搭建好了,可以自己搭建一个了。如果超过5分钟,我就是输家。然后,启动项目,调用接口,查看日志:放开我,从线程名来看,这不是异步?为什么还是tomcat的线程?于是,在研究路上遇到了第一个问题:@Async注解没有生效。为什么不生效?为什么它不起作用?我也糊涂了,我说我以前对这个注解一无所知,我怎么知道的?当你遇到这个问题时你会怎么做?当然是面向浏览器的编程啦!这个地方,如果我从源码上分析为什么不行,肯定能找出原因。但是,如果我为浏览器编程,只用了30秒就找到了这两条信息:失败原因:1、@SpringBootApplication启动类没有添加@EnableAsync注解。2.没有Spring代理类。这是有效调用的地方,先打个断点就可以了:org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor#getExecutorQualifier发起调用后,真的跑到断点了:顺着断点调试,就会来到这个地方:org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor这段代码结构很清晰。编号①处是获取对应方法上@Async注解的值。该值实际上是bean名称。如果不为空,则从Spring容器中获取对应的bean。如果这个值没有值,就是我们的Demo,就会去到编号为②的地方。这个地方就是我要找的默认线程池。最后,是Spring容器中默认的线程池还是我们自定义的线程池。以方法为维度,在map中维护方法与线程池的映射关系。也就是第③步,代码中的executor是一张图:所以,我要找的就是第②处的逻辑。主要有一个defaultExecutor对象:这个东西是一个函数式编程,所以如果你不知道这个东西是干什么的,调试起来可能会有点迷糊:建议你补一下,可以上手10分钟。最后会调试到这个地方:org.springframework.aop.interceptor.AsyncExecutionAspectSupport#getDefaultExecutor这段代码有点意思,就是从BeanFactory中获取一个默认线程池相关的Bean。过程很简单,log也打印的很清楚,这里就不赘述了。但是我想说的有趣的一点是,不知道大家看到这段代码,是否能看出一丝双亲委派的暗示。他们都使用异常和异常中的流程逻辑。上面的“垃圾”代码直接违反了阿里开发规范中的两条:这是源码中的好代码。在业务流程中,这是违反规范的。那么,让我说一句题外话。个人觉得阿里开发规范其实是给我们写业务代码的同仁最好的实践。但是,当这个规模扩展到中间件、基础组件、框架源码的范围时,就会出现一些不可接受的情况。这件事众说纷纭。我觉得阿里开发标准化的idea插件。对于我写增删改查的程序员来说,真是香喷喷的。话不多说,回过头来看看我们拿到的线程池:难道我没有找到我想要的吗?可以看到这个线程池的相关参数。也证实了我之前的猜测:我觉得核心线程数配置为8,队列长度应该是Integer.MAX_VALUE。但是,现在我直接从BeanFactory中获取了这个线程池的bean,那么这个bean是什么时候注入的呢?朋友,不容易吗?这个bean的beanName我已经拿到了,就是applicationTaskExecutor,但是如果你熟练背Spring中获取bean的过程的定型,都知道在这个地方打断点,加上调试条件,然后去慢慢调试。明白了:org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String)假设你只是不知道在上面的地方打断调试?说一个简单粗暴的方法,你拿到了beanName,在代码里一搜就出来了。简单粗暴,效果不错:org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration已经找到这个类了,打个断点就可以开始调试了。先说一个有点坑爹的操作。假设我现在连beaName都不知道,但我知道它一定是Spring管理的线程池。那我就把项目中所有Spring管理的线程池都弄出来,一定有我要找的吧?看下面的截图,当前bean不就是我要找的applicationTaskExecutor吗?这些都是一些野路子,花哨的操作,知道就行,有时会有多种侦查思路。返回类型支持我们已经完成了关于配置的第一个问题。所以,我把Demo程序修改为:再次运行,运行到这个断点,和我们默认的情况不一样。这时候限定符就有价值了:下一步就是去beanFactory获取名字为whyThreadPool的bean。最后拿出来的线程池就是我自定义的线程池:这其实是一个很简单的探索过程,但是背后是有道理的。这是之前有同学问我的问题:其实这个问题还是比较有代表性的。很多同学认为线程池不能滥用,一个项目共用一个就够了。线程池确实不能滥用,但是一个项目中确实可以有多个自定义线程池。根据你的业务场景来划分。举个简单的例子,可以在业务主流程中使用线程池,但是当主流程中某个环节出现问题时,假设需要发送预警信息。发送预警消息的操作可以用另一个线程池来完成。他们可以共享一个线程池吗?是的,它有效。但是会出什么问题呢?假设项目中某个业务出现问题,不断疯狂发送警告信息,甚至线程池爆满。这时候,如果主进程的业务和发送短信使用同一个线程池,会出现什么美好的场景呢?任务一提交就直接走拒绝政策对不对?发送预警短信的辅助功能导致业务失败。本末倒置?所以建议使用两个不同的线程池,各司其职。这其实就是听起来很高大上的线程池隔离技术。那么归结为@Async注释时会发生什么?其实是这样的:那么,还记得我们前面提到的维护方法和线程池映射关系的map吗?就是这样:现在,我运行程序并调用上面的三个方法,目的是将值放入这个映射中:明白了吗?再重复一遍这句话:在方法维度维护方法和线程池的关系。现在,对@Async注解有了一点了解,觉得还是很可爱的。可能以后会考虑在项目中使用。毕竟更符合SpringBoot基于注解开发的编程理念。最后说一句,我看到了,请点赞关注并安排一个,你都安排了我不介意。写文章很累,需要一点积极的反馈。为所有读者朋友敲出一个:
