多线程中ThreadLocal踩坑
前言
记录一下在测试过程中,遇到的一个有关ThreadLocal的问题,顺便学习一下ThreadLocal相关的知识。
ThreadLocal介绍
ThreadLocal是一个关于创建线程局部变量的类。
要点:
- 在当前线程中,任何一个地方都可以访问到ThreadLocal的值。
- 每个线程里面都有一个ThreadLocalMap变量,初始值为null,这个变量的值由ThreadLocal来维护
- 当前线程保存在ThreadLocal中的值只能被当前线程访问,一般情况下其他线程访问不到。
- ThreadLocalMap存储数据方式类似Map的key-value存储方式,只不过ThreadLocal是以当前线程为key,value可以为任意类型的值
问题场景
最近项目需要上线一个大版本,此次版本对前端APP新、老版本发起的请求做了不同的加密处理,经过讨论,需要在后台做版本兼容。
兼容的流程:
- APP端在请求头里面新增一个字段作为新版本APP的标识,如:varA:123
- 后端在SpringDispatcherServlet中判断varA是否为空,若不为空则把它放入ThreadLocal变量中
if (StringUtil.isNotEmpty(varA)){ ThreadContext.put(ThreadContext.FLAG, varA); }
- 然后在JsonHttpMessageConverter(自定义请求解析类)中,根据varA是否为空来决定采取哪种解密方式来解密请求
String flag = ThreadLocal.get(ThreadContext.FLAG); if (StringUtil.isNotEmpty(flag)){ //新版本解密方式 }else{ //老版本解密方式 }
- 逻辑处理
- 响应请求
问题描述
按照上面的兼容流程做完代码更改之后,在本地测试没有问题,但是放在测试环境,由测试人员测试就有问题。
具体问题描述:
- 老版本APP发起的请求在后台解密时会进入新版本APP解密方式的判断里面去,但是只是部分请求才会出现此情况
分析结果
我们知道,后端应用服务器在处理请求时,会对每一个请求分配一个线程来处理,如果每次来一个请求都去新开一个线程,然后响应请求之后又去销毁线程,这样的结果不仅会增加请求响应时间,而且还会大大提高系统资源消耗。
所以为了适应高并发请求,在应用服务器端都会使用线程池来处理请求,效果是减少系统资源开销以及加快请求响应时间。
前面讲到,由于ThreadLocal是以当前线程为key,所以如果前后有两条请求发到后台,并且这两条请求都是使用的线程池里面的同一个线程。并且第一条是新版本APP发过来的带有标识的请求,第二条是老版本APP发过来的不带标识的请求。
第一条请求把标识存入ThreadLocal变量中,在响应完请求之后没有及时的清理掉ThreadLocal中的值
当第二条不带标识的请求到来时,由于在SpringDispatcherServlet中做了不为空才把标识放入ThreadLocal中,所以这里就没有更新ThreadLocal中的值,但其实由于前面一个请求响应之后没有清理掉ThreadLocal中的值,所以在JsonHttpMessageConverter中获取当前线程的标识时,还是有值,这样就会进入新版本的解密方式中去。
问题处理
两种方式:
-
在SpringDispatcherServlet中不做判空处理,从请求中不管获取到什么值都存入ThreadLocal变量中,以此达到实时更新值的效果
-
在响应完请求之后移除ThreadLocal中想要移除的值或清空ThreadLocal里面当前线程保存的所有值
ThreadContext.remove(ThreadContext.FLAG); 或者清空所有 ThreadContext.remove();
最后我采取的第二种方式,因为按逻辑是ThreadLocal里面的数据只适合在本次请求中使用,使用完了之后就得清理掉