你好哇,我是歪歪。
前几天在网上冲浪的时分看到一篇技术文章,讲的是他把一个 request 恳求传递到了线程池里边,然后遇到了一个匪夷所思的状况。
他写了这篇文章,把自己针对这个问题的探究进程共享了出来:
《springboot 中怎样正确的在异步线程中运用request》
www.cnblogs源码交易平台.com/mysgk/p/164…
文章仍是挺不错的,把发现问题和处理问题都写的很明白了。
可是,我觉得把探究问题的部分写的太省掉了,导致我看完之后都不知道这个问题的根本原因是什么。
而为什么我会对这篇文章特别感兴趣呢?
由于这个“坑”我记住我刚刚入行没两年的也遇到过,我现已不记住自己其时是怎样处理的了,可是我必定也没有深化的去研讨。接口类型
由于那个时分遇到问题,就去网上费尽心思的查,粘一个计划过来看能不能用。
假如不能用的话,心里暗骂一句:小可(S)爱(B),然后接着找。
直到找到一个能够用的。
至于为什么能用?
管它呢,研讨这玩意干啥。
首要是其时觉得探究这个源码1688玩意到进入到源码里边去,一涉及到程序员那么可爱源码心里就犯怵,所以就敬而远之。
现在不相同了,现在我看到源码我就觉得振奋,心里想着:多好的素材啊。
已然这次又让我遇到了,所以我决议把几年前的坑填上,盘一盘它。
搞个 Demo
由于appstore这个现象太过匪夷所思,所以写文章的那个老哥以为这个是一个 BUG,还在 Spring 的程序员那么可爱电视剧免费观看 github 上提了一个 issues:
github.c接口om/spring-proj…
这儿边他附上了一个能够复现的 Demo,所以我就直接拿来用了。
的确是能够复现,可是其实他供给的这个 Demo 仍是有点臃肿,具有一点点的迷惑性,直接给我迷晕了,让我在这上面略微花了时刻。
先给你看一下他的 Demo 是怎样样的。
首要是两个 Contappointmentro线程撕裂者ller 接口。
榜首个接口是 get 恳求类型的 getP程序员那么可爱arams,代码很简单,先放在这儿,等下用:
第二个接口是 post 恳求类型的 postTest,就这么几行代码:
@PostMapping("/postTest")
publicStringpostTest(HttpServletRequestrequest){
Stringage1=request.getParameter("age");
Stringname1=request.getParameter("name");
System.out.println("age1="+age1+",name1="+name1);
newThread(newRunnable(){
@Override
publicvoidrun(){
Stringage2=request.getParameter("age");
Stringname2=request.getParameter("name");
System.out.println("age2="+age2+",name2="+name2);
//模拟业务恳求
try{
Thread.sleep(200);
}catch(InterruptedExceptione){
thrownewRuntimeException(e);
}
age2=request.getParameter("age");
name2=request.getParameter("name");
}
}).start();
return"postsuccess";
}
首要是里边启动了一个线程,在线程里边有从 request 里边程序员怎么学获取参数的动作。
这个办法拜访起来是这线程是什么意思样的一个状况:
从 age2、name2 输出上看,虽然 request 传入到异步线程里边了,可是仍是能从里边获取到对应的参数源码1688,没有看出来有appear什么缺点。
可是接下来,匪夷所思的工作就要呈现了。
还记住咱们前面的 getParams 接口吗?
我再把它拿过来给你看一眼:
你说,就这个接口,我用下面这个链接去拜访,在我的认知里边是彻底不或许有任何问题的,对吧?
http://127.0.0.1:8080/getPapproacharams?a=1&b=2
可是,这玩意还真的就打破了我的认知:
在拜访 postTest 办法之后,再次appetite拜访 getParams 办法,getParams 办法居然抛出反常了?
抛出的反常是说我调用的时分没有传递 b 这个参数。
可是我的链接里边分明便是有程序员工作一年后工资 b=2 的啊?
这玩意上哪里说理去?
上面便是那位老哥供给的可复现的 Demo 的首要部分。
可是我前面说了,这个 Demo 有点臃肿,具有程序员是做什么的一点点迷惑性。
首要假如我再加一个输出语句,那么在一个短暂的 sleep 之后, age2 和 name2 就没了线程和进程的区别是什么:
虽然仍是感觉有点奇特吧,可源码网站是也没有刚刚那个操作让我感到震动。
由于从输出 null 这个成果,我至少能够知道程序在这个当线程和进程的区别是什么地就呈现问题了,把问题的规模限定在了一次恳求中。
刚刚那个操作,好家伙,表现出来到状况是这样的:
- 先建议一个 post 恳求,看起来是正常的。
- 然后再建议一个 get 恳求,这个 get 恳求挂了。
- 可是这个 get 恳求从建议的视点来看找不到任何缺点。
你要依据上面这个状况去剖析问题的话,就不好找问题了,究竟要建议两个毫不相干的恳接口英文求才干触发问题。
参加一行输出日志,相当于把问题简化了一点。
可是你看到的是我就加了程序员那么可爱免费观看一行输出日志,实际上等我加这行日志的时分,我拿到这个 Demo 已经过去了好几个小时了。
在这期间我也一向以为必需要按照这个流程来线程是什么意思操作,才干复现问题接口测试用例设计。
所以我才说具有一点接口测试用例设计点迷惑性。
好,现在不管怎样说吧。
我先把 Demo 简化一点,便于持续剖析。我的 Demo 能够简化到这个程度:
@GetMapping("/getTest")
publicStringgetTest(HttpServletRequestrequest){
Stringage=request.getParameter("age");
System.out.println("age="+age);
newThread(()->{
try{
Thread.sleep(200);
}catch(InterruptedExceptione){
thrownewRuntimeException(e);
}
Stringage1=request.getParameter("age");
System.out.println("age1="+age1);
}).start();
return"success";
}
get 和 post 恳求都能够,仅仅我程序员怎么学为了方便选择建议 get 恳求。
然后只需要传递一个参数就行,中心步骤是要把 request 传递到异步线程里边去,调用 getParameter 再次获取对应入参。程序员是做什么的
你能够把上面的代码粘到你本地,把项目跑起来,然后调一次下面这个链接:
http://127.0.0.1:8080/getTest?age=18
从控制台你能够看到这样的输appstore出:
到这儿就复现了前面说的问题。
可是你別着急,你再次建议调用,你会看到控制台的输出是这样的:
怎样样,是不是很奇特,很懵逼?
为了让你愈加直观的懵逼apple,我给你上个动图,建议两次调用,首要重视控制台的输出:
好,现在,你就去泡杯茶,点根烟,渐渐去琢磨,这玩意是不是线程归于超自然现象。
探究
其实我看到这个现象的时分并appetite不是特别的震动,究竟写文章这几年,什么稀奇古怪的现象都遇到过。
所以我仅仅轻蔑一笑,看向了我排查问题的武器库,很快就看到了一个比较趁手的东西:开启源码交易平台 Debug 日志。
假如是曾经,关于这种没有抛出反常的问题跟着,由于没有反常仓库,我必定是迫不及待的正向的 Debug 跟了一下源码,扎到源码里边去一顿狂翻,左看右看。
可是成果常常是一头扎进去之后,很快就迷失了,搞了好几个小时才从源码里边爬出来,出来的时分基本上一无所获。
可是我现在不会这么猴急了,现在就成熟了许多。APP遇到这类问题不会先急着去卷源码会先多从日志里边发掘一接口点东西出来。
所以我遇到这个问题的榜首反响便是调整日志等级到 Debug:
logging.level.root=debug
调查日志这个小技巧我在之前的文章里边也共享过。
当日志调整到 Debug 等级之后,再次建议两次调用,问题复现,同时把日志拿出来做比照。
两次恳求的 Debug 日志全体状况是这样的,左面是榜首次恳求,右边是第2次恳求:
能够看到榜首次恳求比第2次恳求的日志多。
多说明什么问题?
是不是说明榜首接口文档次恳求调用的办法更多一点?
为什么多一点,到底是哪些办法只调用了一次?
我也不知道,可是我能从 Debug 日志里边梳理出来。
比方下面这个图便是梳理出来的榜首次恳求多打印的日志:
很快我就从 Debug 日志里边看到了一个我觉得很可疑的当地:
Start processing with input [age=18]
这一行日志,只要榜首次恳求的时分打印了,从日志表达的意思来看,是处理恳求里边的 age=18。
为什么第2次不打印呢?
我也不知道,可是我知道了榜首个要害断点打在什么方位了。
全局查找要害字 “Start processing with input” 能够找到配置文件里边的 “parameters.bytes”。
然后全局查找 “parameters.bytes”,就能找源码编程器到是在 Parameters.java 文件里边输出的:
也便是这个当地:
org.源码时代apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.n源码网站io.charset.Charset)
找到榜首接口是什么个断点,就找到了突破口,只要好好的拿捏住,之后的工作就基本上就顺风顺水了。
首源码网站要,重启项目,建议调用,在断点处看调用仓库:
接下来的思路是什么?
便是我要从仓库里边找到一个东西。
你想啊线程池面试题,榜首次恳求走这个当地,第2次恳求就不走程序员计算器这个当地了,所以必定有个类似于这样的逻辑:
if(满意某个条件){
走processParameters办法
}
所以,只需要往回找五个调用栈,我就找程序员那么可爱电视剧免费观看到了这一个办法:
org.apache.catalina.connector.Request#getParameter
这个时分你看旁边源码编程器的 paramet程序员工作一年后工资ersParsed 参数是 true,按理来说 true 不应该走进 if 分支呀?
由于这个当地咱们是从断点处的仓库信息往回找,在从程序员工作一年后工资 parseParameters 办法到 processParamete线程撕裂者rs程序员那么可爱 办法之间,必定有当地修改了 parametersParsed 参数的值为 true。
这一点,从 parametersParsed 的初始值是 false 也能看出来:
因而,我决议把第二个断点打在 getParameter 办法中:
再次重启服务,源码时代建议调用,parametersParsed 为 fal线程安全se,开端履行 parseParameters() 办法解析参数线程的几种状态:
而解析参数的意图之一便是把我的 age=18 放到 paramHashValues 这个 Map 容器里边:
org.apache.tomcat.util.http.源码编辑器下载Parameters#a程序员怎么学ddParameter
parseParameters() 办法履行完成之后,接口和抽象类的区别接着从前面的 paramHashValues 容器里边把 age 对应的 18 回来回去:
可是,朋友们,留意上面的图片中有个标号为 ① 的当地:
这个办法,在 parseParameters 办法里边也会被调用:
org.apac程序员是做什么的he.tomcat.util.http.Parameters#handleQuer程序员yParameters
好,现在源码交易平台打起精神来听我说。
handleQueryParameters 办源码编辑器法才是真实解析参数的办法,为了避免重复解析它参加了这样的逻辑:
didQueryParameters 初始为 false,随后被设置为 true。
这个很好了解,入参解析一次就行了,解析的产品一个 Map,后续要拿参数对应的值,从 Map 里边获取即可。
比方我接口和抽象类的区别把入参修改为这样:
http://12appear7.0.0.1:8080/getTest?a=1&b=2&c=接口测试用例设计3&d=4
那么经过解析之后,这个 Map 就变成了这样:
经过了前面的这一顿折腾之后,现在找到了源码精灵永久兑换码解析入参的源码之家办法。
那么全文的要害点就在 didQueryP线程的几种状态arameters 这个参数的变化了。
只要是 false 的时分才会去解析入appreciate参。
那么我接下来的排查思路便是调查approach didQueryParameters 参数appetite的变化,所以在字段上打上断点,重接口测试用例设计启项目,持续调试:
榜源码时代首次进入这个办法的时分 didQueryParameters 为 false,入参是 age=18:
而榜首次进源码编程器入这个办法的原因我前面也说了,是由于触发了 parseParameters 的逻辑:
第2次进入这个办法 didQueryParameters 变为 true 了,不用再次解析:
那么第2次进入这个办法的原因是什么?
前面也说了线程和进程的区别是什么,getParamete源码精灵永久兑换码r 办法的榜首行便是触发解析的逻辑:
接下来,断点停在了这个当线程是什么意思地:
org.apache.tomcat.util.http.Parameters#recycle
办法叫做 recycle,标明接口和抽象类的区别是循环再appointment利用,在APP这儿边会把存放参数的 M程序员那么可爱免费观看ap 清空,把 didQueryParameters 再次设置为了 false。
而当你用同样的手段去调查 parametersParsed 参数,也便是这个参数的时分:
会发现它也有一个 recycle 办法:
org.apache.catalina.connector.Request#recycle
这个办法上的注释,也有一个特别扎眼的词:reuse。
注释过来是这样的:开释一切的目标引证,并初始化实例变量,为从头运用这个目标做准备。
种种迹象标明 request 在 tomcat 里边是循环运用的。
虽然在这之前我也知道是循环接口crc错误计数运用的,可是百闻不如一见嘛。这次是我 Debug 的时分亲眼看到了。
又拿捏一个小细节。
由于咱们在异步线程里边还触发了一次 getParameter 办法:
可是 getTest 办法现已完成了呼应,这个时分 Request 或许现已完成了收回。
留意我说的是“或许”,由于这个时分 Request 的收回动作和异步线程谁先谁后appetite还不必定。
这也解说了这个现象:
虽然 request 传入到异步线程里边了,可是仍是能从里边获取到对应的参数线程的几种状态。
由于此刻 reque源码中的图片st 的收回动作还没做完,还能够持续获取参数。
为了避免这个“或许”,我把线程池 sleep 的时刻调整为 5s,确保 request 完成收回。
然后这异步线程里边持续 Debug,接下来奇源码交易平台特的工作就线程池的七个参数要开端了。
再次触发 handleQueryParameters 的时分,didQueryParameters 由于被 recycle 了,所以变成了程序员工作一年后工资 false。
然后履行解析的逻辑,把 didQueryParameters 设置为 true。
可是,咱们能够看到,此刻查询的内容却没appointment有了,是个 null:
这个也好了解,必定是随着调用结束,被 recycle 了嘛:
所以,到这儿我能解答为什么异步线程里程序员工资一般多少边的输出是 null 了。
queryMB 便是我调用接口crc错误计数的时分传入的 age=18。
经过 Debug 发现异步线程里边调用 getParameter 的时分没有 queryMB ,所以就不会解析出 Map程序员工资一般多少。
没有apple Map ,异步线程里边的输出必approach定是 null。
为什么没有 queryMB 呢?
由于当前这个恳求现已被回程序员那么可爱电视剧免费观看来了,履行了 recycle源码精灵永久兑换码 相关操作,queryMB 便是在这个时分没有的。
那么为什么再次建议调用,会呈现这个奇特approach的现象呢程序员那么可爱免费观看?
很简单,由于在异步线程里边调用 getParameter 的时分,把 didQueryParameters 设置为 true 了。
可是异步源码时代线程里边的调用,超出了 request 的生命周期,所以并不会再次触发 request 的 recycle 相关操作,因而这个 request 拿来复用的时分 didQueryParameters 仍是 true。
所以,从 Debug 来看,虽然 q源码中的图片uappearanceeryMB 是有值的,可是没用啊程序员是做什么的,didQueryParameters 是 true,程序直接 retu程序员计算器rn 了,不会去解析你的入参:
问题得到解答。
此刻,咱们再回到最开端的这个办法中:
你想接口英文想为什么这个办法调用的时分呈现反常了?
仍是相同的道理呀,由于 request 是复用的,虽然你传入了接口英文参数 b,可是由于前一个恳求在异步线程里边调用了 getParameter 办法,将 didQu程序员需要什么学历eryParameters 设置为了 true,导致程序不会去解析我传入接口卡的 a=1&b=2。
从调用链接的视点来说,虽然咱们调用的是这个链接:
http://127.0.0.1:8080/getParams?a=1&b=2
可是关于程序来说,它等效于这个链接:
http://127.0.0.1:8080/getParams
由于入参 b 是 int 类型的,那可不便是程序员那么可爱会抛出这程序员需要什么学历个反常吗:
这个反常是说:哥们,你要么把 b 搞成 Integer 类型的,不传值我就给你赋为 null源码交易平台。要么给我传一个值。
你现在用 in源码中的图片t 来承受,又不给我接口卡值,我这没法处理啊?
我能给你默许赋值一个 0 吗?
必线程池面试题定不能啊,0 和 null 可不是一个含义接口自动化,万一你程序出反常了,把锅甩给我怎approach样办?
算了,我仍是抛线程撕裂者反常吧,最稳妥了。
所以你看,要是你从这个抛反常的当地去找答案,也许能找到,可是路就走远了一点。
由于这个当地并不是问题的根因。
到这儿,你应该清楚这个 BUG 到底是怎样回事了。
request 的生命周期
在探究这个问题的进程线程数是什么中,我也想到了别的一个问题:
一个 request 恳求接口的生命周期是怎样样的?
这题我记住几年前我背过,现在我的确有点想不起来了,可是我知道去哪里approach找答案。
Ja接口和抽象类的区别va线程和进程的区别是什么 Servletapplication Specification,这是一份标准,答案就藏在这个标准里边:
javaee.github.io/servlet-appetitespe…
在 3.13 末节里边,关于 request源码 这个 Object 的生命周期,标准是这样说的:
这寥寥数语,非常要害,所以我一句句的拆解给你看。
Each r程序员那么可爱equestapplication o线程数越多越好吗bject is valid only within the scope of a servlet’s service method, or w线程池ithin the scope of a filappearanceter’s doFilter method,unless the asynchronous processing is enabappleled for the component an源码中的图片d the startAsync method is invoked on the request object.
一上来便是一个长句,可是根本线程撕裂者不要慌。
你知道的,我英语八级半,水平一向是能够的。
先把长句拆短一点,我能够先只翻译 unless 之前的部分程序员那么可爱。
前面这部分说:每个 request 目标只在 servlet 的服务办法的规模内有效,或许在过滤器的 doFi程序员客栈lter 办源码法的规模内有效。
接着它来了一个 unless,表示转折,和 but 差不多接口卡。
咱们首要重视 unless 后边这句:
the asynchronous processing is enabAPPled for the component and the startAsync method is invoked on the request objeappearct.
组件的异步处理功用被启用,并且在 request 上调用了 start接口是什么Async 办法。
也便是说,request 的生命周期在遇到异步的时线程池面试题分有点特殊,可是appearance这个异步又不是我前面演示的那种异步。
关于异步,标准中提程序员怎么学到了 request 里边有个办法:startAsync。
我去看了一眼,果然是有:
回来值是源码之家一个叫做 AsyncContext 的东西。
可是我先按下不表,接着往下翻译。源码编辑器下载
In the case w线程池的七个参数here asynchronous processing occurs, the request object remains valid until complete is invoked on the AsyncCoappstorentext.
在发生异步线程和进程的区别是什么处理的状况下,reque源码编辑器st 目标的生命周期一向会延续到在 AsyncContext 上调用 com接口文档plete 办法之前。
这儿又提到了一个 complete 办法,这个 complete 办法 invoked on the AsyncCon程序员text。
AsyncContext 是什么玩意?
不便是 r接口卡equest.startAsappetiteync(源码之家) 办法的回来值吗?
果然在 AsyncContexappetitet 里边有个 complete 办法:
不慌,持续按下不表,一会就收回,接着往下看。
Containers commonly recycle request objects in order to avoid t源码he performance overhead of request object creation.
容器通常会 rec程序员那么可爱电视剧免费观看ycle 恳求目标,以避免创建恳求目标的功能开销。
看到这个 recycle 咱们就很眼熟了,本来标准里边是建议了容器里边实现 request 的时分尽量复用,而线程的几种状态不是收回,意图是节省功能。
这玩意,归于意外收获呀。
最终一句话是这样的:
Th接口测试e developer must be aware that maintaining r程序员eferences to request objects for which startAsync has not been called outside th线程池面试题e scope described above is not recommend线程池ed as it may hav程序员计算器e indeterminate results.程序员那么可爱
这句话是说:程序员朋友们必需要意识到,我不建议在上述规接口英文模之外维护 request 的引证,由于它或许会发生不确定的成果。
看到这个“不确定的成果”时我很开心,由于我前面现已演示过了,的确会发生不可思议的成果。
可是标准里边在“scope”之前还加了一个限定词:startAsync has not been called。
反过来说,意思便是假如你有一个调用了 s线程是什么意思tartAsync 办法的 request,那么在上述规模之程序员外,你还能够操作这个 reque程序员那么可爱免费观看s源码精灵永久兑换码t,也不会有问题。
这一整段话中,咱们提炼到了两个要害的办法:
- requesAPPt 的 start线程池Async 办法程序员计算器
- AsyncContext 的 complete 办法
依据标准来说,这两个办法才是 requestappetite 异步编程的正确打开方式。
正确打开方式
在这之前,假设你彻底不知道APP startAsync 和 complete 办法。
可是看了标准上的描绘,猜也能猜出来代码应该这样写,然后建议屡次调用,没有任何缺点:
这便是正确的打开方式。
从现象上来说,便是 getTest 恳求回来之后,requ线程是什么意思est 线程并没有被调用 recycle 办法进行收回。
为什么这样写就能实现 request 的异步化呢?
用脚指头想也能想到,必定有一个这样的判别逻辑存在:
if(调用过request的startAsync办法){
先不收回
}
所以,用之前的办法,在 recycle 办法上打断点,并往回找,很快就能找到这个办法:
然后,关于 AsyncContext 的 comple接口测试用例设计te 办法我还留意到它有这样的一个描绘:
也便是说在调用 co程序员怎么学mplete 办法之后 response 流才会关闭,那程序员需要什么学历么有意思的就来了:
我不仅在异步线程里边能够操作 request 还能够操作 response。
可是转念一想,已然都是异线程是什么意思步编程了,操作 response 的含义必定比操作 request 的含义更大。
关于 Tomcat 关于异步恳求的支撑还有许多能够探究的当地,自己渐渐去玩吧。
写到这儿的时分我发现标题说的也不对,标题是:千万不要把 Request 传递到异步线程里边!有坑!
而正确的说法应该是:
千万不要随便把 Request 传递到异步线程里边!有坑!你拿捏不住,得用 startAsync 办法才行。
好了,就这样吧,本文写到这儿就差不多了。
本文首要是共享了一下 request源码中的图片 放到异步线程之后的诡异现象和排查办法,最终也给出了正确的打开方式。
期望你能把握到这源码编辑器样的一个问题排查办法,不要惧怕问题,要抽丝剥茧的干它。
然后,其实和 BUG 排查比起来,关于 request 的异步编程相关的知识愈加重要,本文仅源码编辑器下载仅做了一个小小的引子,假如这块知识对你是空白的,期望你有兴趣的话自己去研讨一下,很有价值。
最终,我想说的是,关于之前文章的一个留言:
从看到这个现象,到写完这篇文章,我源码精灵永久兑换码不断的调试程序,源码1688至少重启了近源码编辑器百次服务,建议了上百次恳求。在源码里边也走了一些弯路,最终才抽丝剥茧的看到本问题的根因。
所以,我排查问题的经验就一个字: