查看原文
其他

彻彻底底给你讲明白啥是SpringMvc异步处理

编程新说李新杰 CSDN云计算 2020-10-16

来源 | 编程新说
责编 | Carol
出品 | CSDN云计算(ID:CSDNcloud)

生活在这个世界上,我们必须承认任何事物都是运动变化着的,没有什么东西是一成不变的。

不仅因为这句话是出自马克思主义哲学的唯物辩证法,而且事实确实如此。下面就来描述这样的一个变化。


分工的产生与协作

想必大家都买过房或终将会买房,那自然离不开装修,就以装修这个话题来展开吧。假设有一个搞装修的人叫小王。

小王活儿做得特别好,但目前还是一个人单打独斗。他正在干活儿的这家业主的邻居发现了他的活儿好,于是专门来找他咨询情况。

小王不得不停下手头的工作,来为潜在的客户解答疑惑或诉说方案。最终靠实力把潜在客户变成了真正的客户。小王的名气就这样传开了。

附近小区的人闻名而来,找他沟通装修方案或确认时间安排,小王不得不每次都停下手头工作,专心为客户解答。等客户走后,他再开始继续工作

随着来找他的人与日俱增,渐渐的小王发现他真正用于干活儿的时间越来越少了,因为他在接待客户上花费了太多的时间。

但没有办法,为了能够接到更多的活儿,他必须为客户提供这个咨询,必须要花掉这些时间。结果导致干活儿的时间被压缩了,活儿干不完了,该咋办呢?

其实解决方法很简单,所有人都能想得到,那就是小王招了一个工人专门给他干活。工人干完活后先过小王这一关,小王觉得可以了才会交付给业主。

小王呢则继续为客户提供咨询服务,在空闲的时候也会去和仅有的一个工人一起干活。看来问题已得到解决,一切重新回到平衡状态中。

由于小王走了狗屎运,每天来的客户实在是太多了,这个刚刚建立的平衡又被打破了。小王不得不全天候的提供咨询,索性不再干活了

而且由于接的活儿越来越多,无奈不得不又招了第二个工人,第三个工人,第四个。。。

我们可以看到很多事情都在逐渐的发生着改变,小王从一个只会干活的工人逐步过渡到只负责接待客户的顾问,不再干活。

为了适应自己的这个新角色,小王不得不招了一个又一个工人为自己干活,这样自己才能得以从原来的工作中彻底解脱。

可见随着事物的发展,分工的产生是一种必然,这就导致了岗位种类的增加。原来只有一个岗位,既负责干活又负责咨询。

现在变为了两个岗位,一个专门负责咨询和监工,其实就是工长了,一个专门负责干活和施工,其实就是工人。

这种“工长 + 工人”的模式在现实生活中使用的非常普遍,工长为工人分配工作和验收工人的工作以及解答工人的问题,其实就是两个岗位之间的协作。

因此,分工的产生与协作代表着一种更加先进和高效的生产方式,至少从理论上来看是这样的,他们各司其职,然后再强强联合,结果可想而知。


Web容器对异步的支持

看完上一小节,希望大家记住这八个字,“各司其职、相互协作”。OK,马上进入程序世界。

SpringMvc的实质就是对Java Web的封装,因此为了更容易的理解SpringMvc的异步,必须先了解Java Web的异步。

Java Web的实质就是一套标准(接口),Web容器实现了这套标准,所以我们站在Web容器的立场来说Java Web的异步,非常好理解。

无论采用什么框架开发的Java Web应用,最终都是在Web容器中运行的,如tomcat就是最常用的Web容器。

启动SpringBoot时,可以看看日志中打印出的线程名称,其实就是tomcat线程池里的线程。可以多请求几次,发现每次处理请求的线程Id都不一样。

我们知道,其实每一个请求过来后,Web容器都会从自己的线程池中拿出一个线程来运行Java Web应用以处理请求。当请求处理完后,这个线程会被还回到线程池中以便处理下一个请求。

如果请求能在非常短的时间内处理完成,是没有问题的。我们设想一下,如果请求执行的非常慢,那么这个线程将无法还回去,于是线程池中可用的线程数目将少一个。

由于线程池的容量是有限的,如果很多执行的很慢的请求同时到来,那么线程池中的线程将会用光,而且短时间内都还不回来。此时的其它请求都将无法被处理。

我们发现执行很快的请求和非常耗时的请求是属于两种不同性质的事物,现在它们都由Web容器的线程池的线程来从头处理到尾,就像当初的小王。

既要接待客户做咨询工作,又要等客户走了自己干活。这显然是低效的,唯一的方法就是将这两种工作分开,分别交由两个岗位去做。

于是我们就看到,小王招了很多工人,自己专业做咨询接活儿,转手把活儿交给工人去做,工人做好后把活儿再交回给小王,小王再向业主交付。

我们可以把这个思路套到Web容器上,就是Web容器线程池的线程只用来处理执行速度很快的请求,这对应于小王只负责咨询接活儿。

对于非常耗时的请求,Web容器线程池的线程将把这个请求交给其它专门的线程池的线程去处理,这对应于小王把接到的活儿交给他的工人去做。

Web容器线程池的线程将再去接受其它请求,因为耗时的请求已经被交出去了。这对应于小王再去接别的活儿,因为上一个活儿已经交给工人去干了。

专门的线程池的线程处理完这个请求后会把它还回给Web容器线程池的线程,这对应于工人干完活儿后会把活儿先交给小王去验收。

Web容器线程池的线程拿到处理结果,把结果写入响应,这个请求就被处理完毕了。这对应于小王把工人干好的活儿交付给业主,施工就算完毕了。

我们来分析一下,小王由于角色转变而带来的工作转变都有哪些。一开始小王单打独斗,小王工作有三种,小王从业主接活儿,小王自己干活,小王向业主交付

到最后小王成了工长,此时的工作是四种,小王从业主接活儿,小王把活儿交给工人去做,工人把做好的活儿交回给小王,小王向业主交付

对比后发现,小王为了使自己不干活,所以引入了工人,以及由此产生的他与工人之间的一去一来的协调交互。

同样来分析一下,Web容器线程池的线程前前后后有哪些变化。一开始线程处理所有的请求,可以分为三个阶段,线程接受请求,线程处理请求,线程写回响应。

到最后线程的工作分为四个阶段,线程接受请求,线程把待处理的请求交给专门的线程池,专门的线程池把处理好的请求交回给线程,线程写回响应

对比后发现,Web容器线程池的线程为了使自己不处理耗时请求,所以引入了专门的线程池,以及由此产生的它与专门的线程池的一去一来的协调交互。

是不是发现线程池和小王的行为是完全对应的。而且他们面临的问题都一样,就是把需要处理的任务交出去给别人,别人把处理完毕的任务再还回给他。

好了,现在我来告诉你,这就是Java Web异步处理的整体逻辑思想。化繁为简后,其实就是一去一来这两个动作罢了。

自己把执行流程交出去

1AsyncContext startAsync()

别人把执行流程还回来

1void dispatch()

上面这两个方法就完成了一去一来这两个动作,仅此而已。

看到这些,可能有些人大失所望,传说中“牛X和高大上”的异步处理,到最后也不过区区十四个字,“执行流程交出去,执行流程还回来”。

可能还会有人觉得,异步处理不应该和客户端也有关系吗?其实并没有,纯粹是服务器端的把戏。而且对于单个请求的处理时间也不会减少。

就像你去饭店吃饭,你点的这份饭在后厨是一个厨师完成的还是多个厨师协作完成的,其实你并不知道,一般情况下你也并不关心。


SpringMvc对异步的支持

上面描述的异步处理逻辑只是一个规范(接口),是不带实现的。任何想要支持Java Web异步处理的,需要在遵守这个规范的前提下,自己提供一套实现。

说白了,就是也要采用交出去还回来的模式,但是如何交出去,怎样还回来,要自己去实现,还有这个专门的线程池也要自己来提供。

SpringMvc提供了对异步的支持,所以它遵守了这个规范,而且它也定义了相似的接口来与Java Web规范交互。

这个接口就是AsyncWebRequest,它的实现类是StandardServletAsyncWebRequest。同样的两个方法。

交出去
1void startAsync()

还回来
1void dispatch()


在这两个方法的实现中,分别去调用了Java Web规范中的对应方法,这就是与Java Web规范的交互。

同时SpringMvc自己是有线程池的,所以在第一个方法里实现了把执行流程交出去的操作,在第二个方法里实现了把执行流程还回来的操作。

还有一个问题就是,并不是所有的方法都需要异步处理,所以就需要有一种方式来告诉SpringMvc,哪个方法需要异步处理。

SpringMvc选择了采用方法返回值的类型来进行区分,凡是@RequestMapping方法的返回值类型是以下这些的,就表明开发人员想进行异步处理,于是SpringMvc就进行异步处理。

1Callable<V>
2
3DeferredResult<T>
4
5WebAsyncTask<V>
6
7StreamingResponseBody
8
9ResponseEntity<StreamingResponseBody>
10
11ResponseBodyEmitter
12
13ResponseEntity<ResponseBodyEmitter>

对于异步处理的方式,其实也包括两大类,一类是开发人员不管,完全交给SpringMvc去处理,一类是开发人员自己把控,不用SpringMvc参与

对于第一类,我们返回Callable<V>类型就可以了,这样SpringMvc会把它提交到线程池里去执行。


对于第二类,
我们返回DeferredResult<T>类型即可,它的意思是延迟结果,就是需要延迟一会才会有结果,但是如何延迟呢,SpringMvc并不会去管。


因此是由开发人员来决定的,开发人员需要自己弄个线程去执行,并且一定要记住
最后要设置一下结果才行。

我们看到耗时的代码都跑到别的线程里去执行了,那么SpringMvc的处理主流程自然就结束了,这就是执行流程交出去的过程。

只不过有一点需要注意,这个请求的处理并没有完成,只是暂时离开了SpringMvc的主流程,在别的线程池里运行着呢。

那么一段时间后,别的线程池运行结束,已经取得了结果,那这个结果和执行流程又该如何还回来呢?

直接还回来吗?显然是不可能的,因为SpringMvc的处理主流程在把任务交出去的那一刻就已经结束了、不存在了。

其实是别的线程池在处理结束并得到结果后,处理流程连同结果会返回到Web容器中,由Web容器再次分派这个请求到SpringMvc中来。

这就是为什么上面第二个方法的名字叫做dispatch的原因,就是再次分派这个请求到Web应用中来嘛,因此会再次进入到SpringMvc中,这不就把执行流程还回来了嘛。

可能会有人产生疑惑,如果SpringMvc再次处理这个请求那不就乱套了吗?答案是显然不会的,因为这次异步的结果已经存在了,自然不会再异步处理了,而是把它跳过去直接进入后续处理。

也就是对方法返回值(ReturnValue)的处理,比如序列化成JSON写入响应,或进行视图渲染,把渲染后的视图写入响应,这样这个请求就算处理完毕了。

推荐阅读:真香,朕在看了!点击“阅读原文”,参与调查

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存