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

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

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

相关文章
|
22天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
100 38
|
20天前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
44 2
|
22天前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
58 4
|
22天前
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
92 2
|
2月前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
137 29
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
1月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
105 0
剖析Tomcat线程池与JDK线程池的区别和联系!
|
2月前
|
Java
直接拿来用:进程&进程池&线程&线程池
直接拿来用:进程&进程池&线程&线程池
|
1月前
|
设计模式 Java 物联网
【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解
【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解
58 0
|
2月前
|
Java
COMATE插件实现使用线程池高级并发模型简化多线程编程
本文介绍了COMATE插件的使用,该插件通过线程池实现高级并发模型,简化了多线程编程的过程,并提供了生成结果和代码参考。
|
2月前
|
监控 Java
线程池中线程异常后:销毁还是复用?技术深度剖析
在并发编程中,线程池作为一种高效利用系统资源的工具,被广泛用于处理大量并发任务。然而,当线程池中的线程在执行任务时遇到异常,如何妥善处理这些异常线程成为了一个值得深入探讨的话题。本文将围绕“线程池中线程异常后:销毁还是复用?”这一主题,分享一些实践经验和理论思考。
129 3