我们知道,ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景。如果用户信息的获取比较昂贵(比如从数据库查询用户信息),那么在 ThreadLocal 中缓存数据是比较合适的做法。但,这么做为什么会出现用户信息错乱的 Bug 呢?

我们看一个具体的案例吧。

使用 Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个 Integer 的值,来暂且代表需要在线程中保存的用户信息,这个值初始是 null。在业务逻辑中,我先从 ThreadLocal 获取一次值,然后把外部传入的参数设置到 ThreadLocal 中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。

    private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);@GetMapping("wrong")public Map wrong(@RequestParam("userId") Integer userId) {//设置用户信息之前先查询一次ThreadLocal中的用户信息String before = Thread.currentThread().getName() + ":" + currentUser.get();//设置用户信息到ThreadLocalcurrentUser.set(userId);//设置用户信息之后再查询一次ThreadLocal中的用户信息Map result;String after = Thread.currentThread().getName() + ":" + currentUser.get();//汇总输出两次查询结果    result = new HashMap();result.put("before", before);result.put("after", after);return result;}

按理说,在设置用户信息之前第一次获取的值始终应该是 null,但我们要意识到,程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的。

顾名思义,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。

为了更快地重现这个问题,我在配置文件中设置一下 Tomcat 的参数,把工作线程池最大线程数设置为 1,这样始终是同一个线程在处理请求:

server.tomcat.max-threads=1

运行程序后先让用户 1 来请求接口,可以看到第一和第二次获取到用户 ID 分别是 null 和 1,符合预期:

随后用户 2 来请求接口,这次就出现了 Bug,第一和第二次获取到用户 ID 分别是 1 和 2,显然第一次获取到了用户 1 的信息,原因就是 Tomcat 的线程池重用了线程。从图中可以看到,两次请求的线程都是同一个线程:http-nio-8080-exec-1。

这个例子告诉我们,在写业务代码时,首先要理解代码会跑在什么线程上:

我们可能会抱怨学多线程没用,因为代码里没有开启使用多线程。但其实,可能只是我们没有意识到,在 Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题

因为线程的创建比较昂贵,所以 Web 服务器往往会使用线程池来处理请求,这就意味着线程会被重用。这时,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。

理解了这个知识点后,我们修正这段代码的方案是,在代码的 finally 代码块中,显式清除 ThreadLocal 中的数据。这样一来,新的请求过来即使使用了之前的线程也不会获取到错误的用户信息了。修正后的代码如下:

    @GetMapping("right")public Map wrong(@RequestParam("userId") Integer userId) {//设置用户信息之前先查询一次ThreadLocal中的用户信息String before = Thread.currentThread().getName() + ":" + currentUser.get();//设置用户信息到ThreadLocalcurrentUser.set(userId);//设置用户信息之后再查询一次ThreadLocal中的用户信息Map result;try {String after = Thread.currentThread().getName() + ":" + currentUser.get();//汇总输出两次查询结果result = new HashMap();result.put("before", before);result.put("after", after);} finally {currentUser.remove();}return result;}

重新运行程序可以验证,再也不会出现第一次查询用户信息查询到之前用户请求的 Bug

ThreadLocal线程复用导致的安全问题相关推荐

  1. ThreadLocal使用时因线程复用导致数据混乱分析

    ThreadLocal使用时因线程复用导致数据混乱分析 本文主要阐述使用ThreadLocal遇到数据混乱情况下的具体分析和解决过程 ThreadLocal原理 网上有很多介绍,不做详细介绍主要有四个 ...

  2. Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失(续)

    前言 上篇文章<Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失>我们对ThreadLocal数据丢失进行了详细的分析,并通过代码的方式复现了这个问题. ...

  3. Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失

    在Spring Cloud中我们用Hystrix来实现断路器,Zuul中默认是用信号量(Hystrix默认是线程)来进行隔离的,我们可以通过配置使用线程方式隔离. 在使用线程隔离的时候,有个问题是必须 ...

  4. 使用ThreadLocal不当可能会导致内存泄露

    使用ThreadLocal不当可能会导致内存泄露 基础篇已经讲解了ThreadLocal的原理,本节着重来讲解下使用ThreadLocal会导致内存泄露的原因,并讲解使用ThreadLocal导致内存 ...

  5. threadlocal线程_线程故事:Web应用程序中的ThreadLocal

    threadlocal线程 本周,我花了一些合理的时间来消除Web应用程序中的所有ThreadLocal变量. 原因是他们造成了类加载器泄漏,我们不能再适当地取消部署我们的应用程序. 取消部署应用程序 ...

  6. 文化袁探索专栏——线程池执行原理|线程复用|线程回收

    文化袁探索专栏--Activity.Window和View三者间关系 文化袁探索专栏--View三大流程#Measure 文化袁探索专栏--View三大流程#Layout 文化袁探索专栏--消息分发机 ...

  7. async spring 默认线程池_springboot-@Async默认线程池导致OOM问题

    转 springboot-@Async默认线程池导致OOM问题 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 前言: 1.最近项目上在测试人员压 ...

  8. 【Android 异步操作】手写 Handler ( Message 消息 | ThreadLocal 线程本地变量 | Looper 中的消息队列 MessageQueue )

    文章目录 一.Message 消息 二.ThreadLocal 线程本地变量 三.Looper 中的消息队列 MessageQueue 一.Message 消息 模仿 Android 中的 Messa ...

  9. 线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?线程池中线程复用原理

    1.一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务.阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使 ...

最新文章

  1. 树莓派练习程序(蜂鸣器)
  2. 发布软件之前,怎样告诉用户怎么用
  3. 读取mysql数据到select_MySQL数据库8(十三)高级数据操作之select指令
  4. 实验二Step1-有序顺序表
  5. [来自软件No1]XP Skin Pack系统主题-把windows 7变回xp的模样
  6. 解决:卸载软件,看我就够了!“启动C:\\Program时出现问题,找不到指定的模块“
  7. C++代码静态分析与优化(10)_rats
  8. numpy和pandas简单使用
  9. SpringCloud Alibaba 实战之《服务门户:Spring Cloud Gateway 如何把好微服务的大门》
  10. oracle cmd窗口 命令行导入*.dmp文件
  11. 搭建mpi测试环境,使用intell的mpi库
  12. 如何进行自媒体创业?你是否能把握住,短视频都有哪些变现方式?
  13. adroid xpose 修改java方法实例_基于xposed 修改硬件信息(xposed框架使用)
  14. 01.消消乐填充的算法
  15. 国科大学习资料--模式识别与机器学习(黄庆明)--期末复习题4(含答案)
  16. Summary_HTML中让两个div并排显示
  17. Tonya mitchell - Stay
  18. 商品亲和性分析与关联规则挖掘
  19. word2007文档结构图的颜色如何去掉
  20. 惠普之路——HP公司发展史

热门文章

  1. 近世代数--整环--高斯整环
  2. shell编程之数学运算
  3. [How TO]-windows安装wget工具
  4. [download]-软件下载地址-百度网盘
  5. Linux Kernel aarch64的ARM-CE aes-ecb的底层代码导读
  6. DNS_ARP_DHCP协议
  7. 2020-11-13(c++下JNI开发不同点)
  8. 17、MySQL创建,执行事件
  9. 3、 AUTO_INCREMENT:主键自增长
  10. 7、MySQL数据类型的选择