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

简介: 之前在公司做的一个项目中,有一个 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月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
83 1
|
1月前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
108 29
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
13天前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
|
1月前
|
Java
直接拿来用:进程&进程池&线程&线程池
直接拿来用:进程&进程池&线程&线程池
|
16天前
|
设计模式 Java 物联网
【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解
【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解
34 0
|
28天前
|
Java
COMATE插件实现使用线程池高级并发模型简化多线程编程
本文介绍了COMATE插件的使用,该插件通过线程池实现高级并发模型,简化了多线程编程的过程,并提供了生成结果和代码参考。
|
1月前
|
监控 Java
线程池中线程异常后:销毁还是复用?技术深度剖析
在并发编程中,线程池作为一种高效利用系统资源的工具,被广泛用于处理大量并发任务。然而,当线程池中的线程在执行任务时遇到异常,如何妥善处理这些异常线程成为了一个值得深入探讨的话题。本文将围绕“线程池中线程异常后:销毁还是复用?”这一主题,分享一些实践经验和理论思考。
97 3
|
2月前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
49 2
|
2月前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
|
2月前
|
Java
线程池中线程抛了异常,该如何处理?
【8月更文挑战第27天】在Java多线程编程中,线程池(ThreadPool)是一种常用的并发处理工具,它能够有效地管理线程的生命周期,提高资源利用率,并简化并发编程的复杂性。然而,当线程池中的线程在执行任务时抛出异常,如果不妥善处理,这些异常可能会导致程序出现未预料的行为,甚至崩溃。因此,了解并掌握线程池异常处理机制至关重要。
287 0