线程池中线程重用导致的问题

简介: 之前在公司做的一个项目中,有一个 core 的公共依赖包,那个依赖里面简单封装了用户的信息。

线程池中线程重用导致的问题

之前在公司做的一个项目中,有一个 core 的公共依赖包,那个依赖里面简单封装了用户的信息。

突然有一天在生产上遇到一个奇怪的问题,有时获取到的用户信息是别人的。我就下把 core 包代码下载下来,查看代码后,我发现使用了 ThreadLocal 来缓存获取到的用户信息。

ThreadLocal 是用于变量在线程间隔离,前端通过加密的 token ,core 包把 token 解密成用户信息放在 ThreadLocal 中也没什么问题的。但,这么做为什么会出现用户信息错乱的 Bug 呢?

自己写个 demo 试试

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

        @GetMapping("/wrong/{token}")
   public Map<String, String> userInfo(@PathVariable String token) {
    
    
       ThreadLocal<String> userinfo = new ThreadLocal<>();
       Map<String, String> map = new HashMap<>();
       map.put("before", userinfo.get());
       userinfo.set(token);
       map.put("after", userinfo.get());
       map.put("threadName", Thread.currentThread().getName());
       return map;
   }

我们执行一下,看看结果

image-20230725152924121

从结果中我们可以看到,我们请求了两次接口,token 分别是 user1 和 user2 , 返回换结果是没有任何问题的, before 的值为 null, after 的值为当前用户的 token 。但是我们知道, tomcat 容器用的也是线程池来处理这些请求的, 如果第一个请求和第二个请求用的是同一个线程会出现什么情况呢。为了出现这种情况,我们修改一下 tomcat 的配置:

server.tomcat.max-threads=1

重新请求一下,看结果

image-20230725163055758

image-20230725163115359

从上面我们可以看到第一次请求是没有什么问题,问题就在于同一个线程在第二次请求的时候是存着user1的信息的。

这个demo是比较极端的,主要是告诉大家线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。

所以通过这个示例告诉大家,我们在写业务代码时,第一点就是要理解代码会跑在什么线程上:我们可能会抱怨学多线程没用,因为代码里没有开启使用多线程。但可能只是我们没有意识到,在 Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题。存在线程池的地方就意味着线程会被重用。使用 ThreadLocal 这种和线程强绑定的工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。

下面我们就根据上面的原则来改造一下代码,让它能够按照我们的预期运行:在最后的地方我们加入代码来显式地清除 ThreadLocal 里面的东西

       @GetMapping("/right/{token}")
   public Map<String, String> userInfo(@PathVariable String token) {
    
    
       Map<String, String> map = new HashMap<>();
       map.put("before", userinfo.get());
       userinfo.set(token);
       try {
    
    
           map.put("after", userinfo.get());
           map.put("threadName", Thread.currentThread().getName());
           return map;
       } finally {
    
    
           userinfo.remove();
       }
   }

再次请求一下看看效果

image-20230725165117947

image-20230725165134604

这样就不会出现在此线程中存在着上次请求的数据。

相关文章
|
5天前
|
Java 数据库 Android开发
【专栏】Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理
【4月更文挑战第27天】本文探讨了Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理。通过案例分析展示了网络请求、图像处理和数据库操作的优化实践。同时,文章指出并发编程的挑战,如性能评估、调试及兼容性问题,并强调了多线程优化对提升应用性能的重要性。开发者应持续学习和探索新的优化策略,以适应移动应用市场的竞争需求。
|
5天前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
17 1
|
5天前
|
Java 程序员 数据库
Java线程池让使用线程变得更加高效
使用一个线程需要经过创建、运行、销毁三大步骤,如果业务系统每个线程都要经历这个过程,那会带来过多不必要的资源消耗。线程池就是为了解决这个问题而生,需要时就从池中拿取,使用完毕就放回去,池化思想通过复用对象大大提高了系统的性能。线程池、数据库连接池、对象池等都采用了池化技术,下面我们就来学习下线程池的核心知识、面试重点~
62 5
Java线程池让使用线程变得更加高效
|
5天前
|
消息中间件 监控 前端开发
面试官:核心线程数为0时,线程池如何执行?
线程池是 Java 中用于提升程序执行效率的主要手段,也是并发编程中的核心实现技术,并且它也被广泛的应用在日常项目的开发之中。那问题来了,如果把线程池中的核心线程数设置为 0 时,线程池是如何执行的? 要回答这个问题,我们首先要了解在正常情况下,线程池的执行流程,也就是说当有一个任务来了之后,线程池是如何运行的? ## 1.线程池的执行流程 正常情况下(核心线程数不为 0 的情况下)线程池的执行流程如下: 1. **判断核心线程数**:先判断当前工作线程数是否大于核心线程数,如果结果为 false,则新建线程并执行任务。 2. **判断任务队列**:如果大于核心线程数,则判断任务队列是否
24 1
面试官:核心线程数为0时,线程池如何执行?
|
5天前
|
监控 安全 Java
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
|
5天前
|
监控 Java 调度
Java多线程实战-从零手搓一个简易线程池(四)线程池生命周期状态流转实现
Java多线程实战-从零手搓一个简易线程池(四)线程池生命周期状态流转实现
|
5天前
|
设计模式 Java
Java多线程实战-从零手搓一个简易线程池(三)线程工厂,核心线程与非核心线程逻辑实现
Java多线程实战-从零手搓一个简易线程池(三)线程工厂,核心线程与非核心线程逻辑实现
|
5天前
|
缓存 Java 调度
为什么要使用线程池?线程创建不好使?
为什么要使用线程池?线程创建不好使?
20 0
|
5天前
|
Java 测试技术
Java多线程实战-从零手搓一个简易线程池(二)线程池实现与拒绝策略接口定义
Java多线程实战-从零手搓一个简易线程池(二)线程池实现与拒绝策略接口定义
|
5天前
|
存储 安全 Java
Java多线程实战-从零手搓一个简易线程池(一)定义任务等待队列
Java多线程实战-从零手搓一个简易线程池(一)定义任务等待队列