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

简介: 之前在公司做的一个项目中,有一个 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

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

相关文章
|
2天前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
2月前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
229 64
|
2月前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
126 38
|
2月前
|
Java
.如何根据 CPU 核心数设计线程池线程数量
IO 密集型:核心数*2 计算密集型: 核心数+1 为什么加 1?即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。
91 4
|
2月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
132 2
|
2月前
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
468 2
|
3月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
202 0
剖析Tomcat线程池与JDK线程池的区别和联系!
|
4月前
|
Java
直接拿来用:进程&进程池&线程&线程池
直接拿来用:进程&进程池&线程&线程池
|
3月前
|
设计模式 Java 物联网
【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解
【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解
76 0
|
4月前
|
Java
COMATE插件实现使用线程池高级并发模型简化多线程编程
本文介绍了COMATE插件的使用,该插件通过线程池实现高级并发模型,简化了多线程编程的过程,并提供了生成结果和代码参考。

相关实验场景

更多