多线程中ThreadLocal踩坑

前言

记录一下在测试过程中,遇到的一个有关ThreadLocal的问题,顺便学习一下ThreadLocal相关的知识。

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里面的数据只适合在本次请求中使用,使用完了之后就得清理掉

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,099评论 19 139
  • 线程池ThreadPoolExecutor corepoolsize:核心池的大小,默认情况下,在创建了线程池之后...
    irckwk1阅读 777评论 0 0
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,688评论 8 265
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,871评论 3 53
  • 梦想是一辈子路,目标是一阵子路 从小到大,老师和家长都常说:一个人应该拥有梦想。那个时候我们误以为想吃一个蛋糕或...
    丶烟花雨阅读 276评论 0 0