多线程中ThreadLocal踩坑

转自:https://www.jianshu.com/p/66a5dea80070

ThreadLocal介绍

ThreadLocal是一个关于创建线程局部变量的类。

要点:

  • 在当前线程中,任何一个地方都可以访问到ThreadLocal的值。
  • 每个线程里面都有一个ThreadLocalMap变量,初始值为null,这个变量的值由ThreadLocal来维护
  • 当前线程保存在ThreadLocal中的值只能被当前线程访问,一般情况下其他线程访问不到。
  • ThreadLocalMap存储数据方式类似Map的key-value存储方式,只不过ThreadLocal是以当前线程为keyvalue可以为任意类型的值

问题场景

项目需要上线一个大版本,此次版本对前端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里面的数据只适合在本次请求中使用,使用完了之后就得清理掉。

你可能感兴趣的:(多线程中ThreadLocal踩坑)