Java避坑案例 - 忽略线程重用导致信息错乱

news2025/1/19 20:19:14

文章目录

  • Pre
  • 导读
  • 问题背景
  • 问题重现
    • 存在Bug的代码
    • BUG现象
  • 问题分析
  • 解决方案
    • 修正后的代码
    • 修正后的现象
  • ThreadLocal 的正确使用
  • 小结

在这里插入图片描述


Pre

并发编程-11线程安全策略之线程封闭

Spring JDBC-Spring事务管理之ThreadLocal基础知识

每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal

Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例&源码分析

J.U.C Review - ThreadLocal原理源码分析

Java Review - 并发编程_ThreadLocalRandom实现原理&源码分析


导读

  1. 问题背景:利用 ThreadLocal 缓存用户信息的代码在生产环境下出现用户信息错乱。
  2. 问题分析:由于 Tomcat 使用线程池处理请求,导致线程重用时 ThreadLocal 的值未及时清理。
  3. 关键点:理解线程池如何影响 ThreadLocal 数据隔离,并明确如何在多线程环境下正确使用它。
  4. 解决方案:通过 finally 块确保在请求结束后移除数据,避免数据污染。

问题背景

使用 Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个 Integer 的值,
来暂且代表需要在线程中保存的用户信息,这个值初始是 null


问题重现

业务逻辑: 在业务逻辑中,先从ThreadLocal 获取一次值,然后把外部传入的参数设置到 ThreadLocal 中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。


存在Bug的代码

private ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);

@GetMapping("/badDemo")
public Map badDemo(@RequestParam("userId") Integer userId) {
    // 第一次获取ThreadLocal中的用户信息
    String before = Thread.currentThread().getName() + ":" + currentUser.get();
    // 将当前请求的用户ID设置到ThreadLocal中
    currentUser.set(userId);
    // 第二次获取ThreadLocal中的用户信息
    String after = Thread.currentThread().getName() + ":" + currentUser.get();

    Map<String, String> result = new HashMap<>();
    result.put("before", before);
    result.put("after", after);
    return result;
}

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


BUG现象

  1. 第一次请求(用户1):
    before: http-nio-8080-exec-1:null  
    after: http-nio-8080-exec-1:1
    
  2. 第二次请求(用户2):
    before: http-nio-8080-exec-1:1  
    after: http-nio-8080-exec-1:2
    

问题:用户2的请求读取到了用户1的用户ID,说明ThreadLocal中的数据未被清除。

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

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


问题分析

ThreadLocal 的设计初衷是确保每个线程都有自己独立的变量副本。然而,当 ThreadLocal 与线程池结合使用时,需要特别小心线程重用的问题。

  • Tomcat线程池:Tomcat 使用固定线程池来处理并发请求,避免频繁创建和销毁线程。线程执行完一次请求后会被重用处理新的请求。
  • ThreadLocal 的问题:当线程处理完请求后,如果没有及时调用 remove() 方法清理数据,后续请求重用该线程时,可能会读取到之前请求遗留的数据。

解决方案

finally 块中调用 remove() 确保线程重用时不会带有上一次请求的残留数据。

修正后的代码

@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
    String before = Thread.currentThread().getName() + ":" + currentUser.get();
    currentUser.set(userId);
    try {
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        Map<String, String> result = new HashMap<>();
        result.put("before", before);
        result.put("after", after);
        return result;
    } finally {
        // 确保请求结束后清理ThreadLocal中的数据
        currentUser.remove();
    }
}

修正后的现象

  1. 第一次请求(用户1):
    before: http-nio-8080-exec-1:null  
    after: http-nio-8080-exec-1:1
    
  2. 第二次请求(用户2):
    before: http-nio-8080-exec-1:null  
    after: http-nio-8080-exec-1:2
    

ThreadLocal 的正确使用

  • 及时清理在使用 ThreadLocal 时,无论请求是否正常完成,都需要在 finally 块中调用 remove() 清理数据。
  • 线程池环境注意事项:因为线程池中的线程会被多次重用,所以不适合将请求级数据长期存储在 ThreadLocal 中。
  • 线程安全容器的使用
    如果确实需要在多个线程之间共享数据,可以使用 Java 并发包中的线程安全容器,如:
    • ConcurrentHashMap:适用于需要高效读写的场景。
    • CopyOnWriteArrayList:适合读多写少的场景。

小结

  • 问题根因ThreadLocal 数据未及时清理,导致线程重用时数据错乱。
  • 修正方案:在 finally 块中调用 remove() 方法清理数据,确保每次请求都是干净的上下文。
  • 最佳实践:在 Web 应用中,谨慎使用 ThreadLocal,并在需要共享数据时选择合适的线程安全容器。

教训: 只知道使用并发工具,但并不清楚当前线程的来龙去脉,解决多线程问题却不了解线
程。比如,使用 ThreadLocal 来缓存数据,以为 ThreadLocal 在线程之间做了隔离不会有线程安全问题,没想到线程重用导致数据串了。

请务必记得,在业务逻辑结束之前清理ThreadLocal 中的数据。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2213982.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

忘记7-zip文件7-zip文件,还可以解压zip文件吗?

文件压缩与解压已成为我们日常处理数据和存储信息的常规操作。7-Zip&#xff0c;作为一款开源且功能强大的文件压缩工具&#xff0c;凭借其高压缩率、支持多种格式以及免费使用的特点&#xff0c;赢得了广大用户的青睐。然而&#xff0c;出于保护文件内容安全的考虑&#xff0c…

echart 采坑记录

1、刷新数据的时候使用setOption myChart.setOption(option,true); 第二个参数为true才会刷新数据 2、react引入ehcart&#xff0c;

[C++] 红黑树的实现:原理与底层解析

文章目录 [toc] 红黑树的概念红黑树的规则红黑树如何确保最长路径不超过最短路径的2倍红黑树规则最短路径与最长路径的分析最短路径&#xff1a;全黑路径最长路径&#xff1a;红黑交替路径 结论&#xff1a;红黑树的平衡性如何保障操作效率 红黑树的实现红黑树的节点结构红黑树…

【性能测试】jmeter通过定时器来模拟接口的动态请求时机

在 Apache JMeter 中使用定时器组件来模拟真实用户行为是非常重要的&#xff0c;因为这有助于确保你的性能测试结果更加贴近实际情况。 1. 固定定时器&#xff08;Constant Timer&#xff09; 这是最基础的定时器&#xff0c;用于在每个样本之间增加固定的时间延迟。然而&…

OpenCV-物体跟踪

文章目录 一、物体跟踪的定义二、OpenCV中的物体跟踪算法三、OpenCV物体跟踪的实现步骤四、代码实现五、注意事项 OpenCV是一个开源的计算机视觉和机器学习软件库&#xff0c;它提供了丰富的功能来实现物体跟踪。以下是对OpenCV中物体跟踪的详细解释&#xff1a; 一、物体跟踪的…

微服务架构是如何运作的?

在当今的软件架构领域&#xff0c;微服务架构以其灵活性、可扩展性和高可用性等优势&#xff0c;受到了越来越多企业和开发者的青睐。那么&#xff0c;微服务架构究竟是如何运作的呢&#xff1f;让我们一起来深入了解。 一、微服务架构的基本概念 微服务架构是一种将单个应用…

QT开发--网络编程

第十七章 QT网络编程 Qt Network模块用于TCP/IP编程&#xff0c;提供HTTP请求、cookies、DNS等功能的C类。 使用需在pro文件中添加“QT network”。 tcp通信流程图 17.1 QHostInfo QHostInfo类用于查找主机名与IP地址的关联。它提供两种查找方式&#xff1a; 1、异步查找&…

windows10创建系统账户

方式一: .a.点击 windows 图标->设置 .b.点击账户 .c.点击其他账户->将其他人添加到这台电脑->弹出对话框 .d.选中用户->右键->新用户->输入用户名和密码->创建 如下创建完成 方式二&#xff1a; windows R 输入lusrmgr.msc 后续操作步骤同上。 修改用…

YOLO11改进|SPPF篇|引入FocalModulation特征金字塔

目录 一、【FocalModulation】特征金字塔1.1【FocalModulation】特征金字塔介绍1.2【FocalModulation】核心代码二、添加【FocalModulation】特征金字塔2.1STEP12.2STEP22.3STEP32.4STEP4三、yaml文件与运行3.1yaml文件3.2运行成功截图一、【FocalModulation】特征金字塔 1.1【…

Linux网络命令:用于配置防火墙规则的一个用户友好的工具ufw详解

目录 一、概述 二、安装 UFW 三、启动、重启和关闭 UFW 1、启动 2、关闭UFW 3、 重启 UFW 四、查看 UFW 状态 五、UFW 基本命令 1. 允许端口 &#xff08;1&#xff09;单个 TCP 端口 &#xff08;2&#xff09;允许单个 UDP 端口 &#xff08;3&#xff0…

MySQL增删改进阶

目录 1.数据库约束 1.1约束类型 1.2 not null约束 1.3 unique&#xff1a;唯一约束 1.4 default&#xff1a;默认约束 1.5 primary key&#xff1a;主键约束 1.6 foreign key:外键约束 1.7 check约束&#xff08;了解&#xff09; 2.表的设计 3.新增&#xff08;进阶&…

CUDA 运行时GPU信息查询

cuda 官网文档名&#xff1a;CUDA_Runtime_API 运行时API查询GPU信息 调用 cudaDeviceProp prop; cudaGetDeviceProperties(&prop, device_id) 定义 由此可见&#xff0c;只能在主机上调用。 #include <cuda_runtime.h>#include <iostream> #include <…

Apache Kafka消息传递策略

kafka消息传递策略 微信公众号&#xff1a;阿俊的学习记录空间小红书&#xff1a;ArnoZhangwordpress&#xff1a;arnozhang1994博客园&#xff1a;arnozhangCSDN&#xff1a;ArnoZhang1994 现在我们了解了一些关于生产者和消费者的工作原理&#xff0c;接下来讨论Kafka在生产…

Java:玩家打怪小游戏

今天&#xff0c;我们尝试用Java来做一个“打怪小游戏”&#xff0c;听名字就知道&#xff0c;我们是应该创建几个成员和怪物&#xff0c;还有知道知道成员和怪物的血量&#xff0c;一次攻击的伤害等等。。当然我们的游戏攻击模式是“回合制”&#xff08;其实是别的方法&#…

SpringCloud-OpenFeign-服务接口调用

是什么 把需要暴露的api使用接口来暴露&#xff0c;客户端需要调用的时候&#xff0c;直接查看这个接口中有没有就可以了 通用步骤 架构说明 common模块 common 引入 openfeign 新建服务接口类 FeignClient(value "cloud-payment-service") // 服务名 public i…

【数据采集工具】Flume从入门到面试学习总结

国科大学习生活&#xff08;期末复习资料、课程大作业解析、大厂实习经验心得等&#xff09;: 文章专栏&#xff08;点击跳转&#xff09; 大数据开发学习文档&#xff08;分布式文件系统的实现&#xff0c;大数据生态圈学习文档等&#xff09;: 文章专栏&#xff08;点击跳转&…

# linux从入门到精通-从基础学起,逐步提升,探索linux奥秘(十三)--权限设置注意事项和属主属组设置sudo操作

linux从入门到精通-从基础学起&#xff0c;逐步提升&#xff0c;探索linux奥秘&#xff08;十三&#xff09;–权限设置注意事项和属主属组设置sudo操作 一、linux 权限设置 特殊注意事项 1、使用root用户创建一个文件夹&#xff08;/oo&#xff09;&#xff0c;权限默认&…

前端知识点总和

目录 一、canvas&#xff1a; &#xff08;1&#xff09;创建canvas标签&#xff1a; &#xff08;2&#xff09;使用JS获得这个canvas标签的DOM对象&#xff1a; &#xff08;3&#xff09;决定是画二维还是三维的画&#xff1a; &#xff08;4&#xff09;API&#xff1…

企业防止信息泄露的措施有哪些?10个防止信息泄露小技巧分享给你

在数字化时代&#xff0c;企业面临的安全挑战日益严峻&#xff0c;尤其是信息泄露问题。一旦企业内部或外部的敏感信息遭到泄露&#xff0c;不仅会造成巨大的经济损失&#xff0c;还可能影响企业声誉、客户信任&#xff0c;甚至可能引发法律纠纷。为了有效防止信息泄露&#xf…

使用 SQLmap 自动化检测 SQL 注入

使用 SQLmap 自动化检测 SQL 注入是一种常见的渗透测试技术。SQLmap 是一个强大的开源工具&#xff0c;可以自动检测和利用 SQL 注入漏洞&#xff0c;提取数据库信息&#xff0c;并接管目标数据库服务器。下面是如何使用 SQLmap 进行自动化检测 SQL 注入的基本步骤。 准备环境…