当前位置: 首页 > news >正文

ThreadLocal线程重用导致用户信息错乱的 Bug

在生产上遇到一个诡异的问题,有时获取到的用户信息是别人的。查看代码后,我发现他使用了 ThreadLocal 来缓存获取到的用户信息。

我们知道,ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景。如果用户信息的获取比较昂贵(比如从数据库查询用户信息),那么在 ThreadLocal 中缓存数据是比较合适的做法。但,这么做为什么会出现用户信息错乱的 Bug 呢?

我们看一个具体的案例吧。

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

private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {//设置用户信息之前先查询一次ThreadLocal中的用户信息String before  = Thread.currentThread().getName() + ":" + currentUser.get();//设置用户信息到ThreadLocalcurrentUser.set(userId);//设置用户信息之后再查询一次ThreadLocal中的用户信息String after  = Thread.currentThread().getName() + ":" + currentUser.get();//汇总输出两次查询结果Map result = new HashMap();result.put("before", before);result.put("after", after);return result;
}

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

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

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

server.tomcat.max-threads=1

运行程序后先让用户 1 来请求接口,可以看到第一和第二次获取到用户 ID 分别是 null 和 1,符合预期:

 随后用户 2 来请求接口,这次就出现了 Bug,第一和第二次获取到用户 ID 分别是 1 和 2,显然第一次获取到了用户 1 的信息,原因就是 Tomcat 的线程池重用了线程。从图中可以看到,两次请求的线程都是同一个线程:http-nio-8080-exec-1。

这个例子告诉我们,在写业务代码时,首先要理解代码会跑在什么线程上:

  • 我们可能会抱怨学多线程没用,因为代码里没有开启使用多线程。但其实,可能只是我们没有意识到,在 Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题。 
  • 因为线程的创建比较昂贵,所以 Web 服务器往往会使用线程池来处理请求,这就意味着线程会被重用。这时,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。

理解了这个知识点后,我们修正这段代码的方案是,在代码的 finally 代码块中,显式清除 ThreadLocal 中的数据。这样一来,新的请求过来即使使用了之前的线程也不会获取到错误的用户信息了。修正后的代码如下:

@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 result = new HashMap();result.put("before", before);result.put("after", after);return result;} finally {//在finally代码块中删除ThreadLocal中的数据,确保数据不串currentUser.remove();}
}

重新运行程序可以验证,再也不会出现第一次查询用户信息查询到之前用户请求的 Bug:

总结:  使用 ThreadLocal 来缓存数据,以为 ThreadLocal 在线程之间做了隔离不会有线程安全问题,没想到线程重用导致数据串了。请务必记得,在业务逻辑结束之前清理 ThreadLocal 中的数据。

相关文章:

ThreadLocal线程重用导致用户信息错乱的 Bug

在生产上遇到一个诡异的问题&#xff0c;有时获取到的用户信息是别人的。查看代码后&#xff0c;我发现他使用了 ThreadLocal 来缓存获取到的用户信息。 我们知道&#xff0c;ThreadLocal 适用于变量在线程间隔离&#xff0c;而在方法或类间共享的场景。如果用户信息的获取比较…...

洛谷——P1143 进制转换

文章目录 一、题目进制转换题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 二、题解基本思路&#xff1a; 一、题目 进制转换 题目描述 请你编一程序实现两种不同进制之间的数据转换。 输入格式 共三行&#xff0c;第一行是一个正整数&#xff0c;表示需要转换的…...

linux stop_machine 停机机制应用及一次触发 soft lockup 分析

文章目录 stop_mchine 引起的 soft lockup触发 soft lockup 原因分析&#xff08;一&#xff09;&#xff1a;触发 soft lockup 原因分析&#xff08;二&#xff09;触发 soft lockup 原因分析&#xff08;三&#xff09; stop_mchine 引起的 soft lockup 某次在服务器上某节点…...

ARM 链接器优化功能介绍

消除公共部分组 链接器可以检测节组的多个副本&#xff0c;并丢弃其他副本。 Arm Compiler for Embedded 生成用于链接的完整对象。因此&#xff1a; 如果 C 和 C 源代码中存在内联函数&#xff0c;则每个对象都包含该对象所需的内联函数的外联副本。如果在 C 源代码中使用…...

动手学深度学习之卷积神经网络之池化层

池化层 卷积层对位置太敏感了&#xff0c;可能一点点变化就会导致输出的变化&#xff0c;这时候就需要池化层了&#xff0c;池化层的主要作用就是缓解卷积层对位置的敏感性 二维最大池化 这里有一个窗口&#xff0c;来滑动&#xff0c;每次我们将窗口中最大的值给拿出来 还是上…...

HackTheBox - Medium - Linux - Ambassador

Ambassador Ambassador 是一台中等难度的 Linux 机器&#xff0c;用于解决硬编码的明文凭据留在旧版本代码中的问题。首先&#xff0c;“Grafana”CVE &#xff08;“CVE-2021-43798”&#xff09; 用于读取目标上的任意文件。在研究了服务的常见配置方式后&#xff0c;将在其…...

嵌入式——循环队列

循环队列 (Circular Queue) 是一种数据结构(或称环形队列、圆形队列)。它类似于普通队列,但是在循环队列中,当队列尾部到达数组的末尾时,它会从数组的开头重新开始。这种数据结构通常用于需要固定大小的队列,例如计算机内存中的缓冲区。循环队列可以通过数组或链表实现,…...

2024.1.7-实战-docker方式给自己网站部署prometheus监控ecs资源使用情况-2024.1.7(测试成功)

实战-docker方式给自己网站部署prometheus监控ecs资源使用情况-2024.1.7(测试成功) 目录 最终效果 原文链接 https://onedayxyy.cn/docs/prometheus-grafana-ecs 参考模板 https://i4t.com/ https://grafana.frps.cn &#x1f530; 额&#xff0c;注意哦: 他这个是通过frp来…...

20240107 SQL基础50题打卡

20240107 SQL基础50题打卡 1978. 上级经理已离职的公司员工 表: Employees ----------------------- | Column Name | Type | ----------------------- | employee_id | int | | name | varchar | | manager_id | int | | salary | int | -…...

阿里云公网带宽出网和入网是什么?上行和下行是什么?

什么是阿里云服务器ECS的入网带宽和出网带宽&#xff1f;以云服务器为中心&#xff0c;流入云服务器占用的带宽是入网带宽&#xff0c;流量从云服务器流出的带宽是出网带宽。阿里云服务器网aliyunfuwuqi.com分享入网带宽和出网带宽说明表&#xff1a; 带宽类别说明入网带宽&am…...

eureka工作原理是什么

EUREKA 是一个基于 RESTful 风格的服务发现系统&#xff0c;它主要用于帮助实现在微服务架构中的服务自动发现与注册。其工作原理主要包括以下几个步骤&#xff1a; 注册中心&#xff1a;EUREKA 中有一个集中的注册中心&#xff0c;所有的服务都将在此注册和发现。注册中心可以…...

Vue中的事件委托(事件代理)使用方法介绍

事件委托&#xff08;事件代理&#xff09; 将原本需要绑定在子元素上的事件监听器委托在父元素上&#xff0c;让父元素充当事件监听的职务。 事件委托是一种利用事件冒泡的特性&#xff0c;在父节点上响应事件&#xff0c;而不是在子节点上响应事件的技术。它能够改善性能&a…...

「HDLBits题解」Wire decl

本专栏的目的是分享可以通过HDLBits仿真的Verilog代码 以提供参考 各位可同时参考我的代码和官方题解代码 或许会有所收益 题目链接&#xff1a;Wire decl - HDLBits default_nettype none module top_module(input a,input b,input c,input d,output out,output out_n ); w…...

[MAUI]在.NET MAUI中调用拨号界面

在.NET MAUI中调用拨号界面 前置要求: Visual Studio 2022 安装包“.NET Multi-platform App UI 开发” 参考文档: 电话拨号程序 新建一个MAUI项目 在解决方案资源管理器窗口中找到Platforms/Android/AndroidManifest.xml在AndroidManifest.xml中添加下文中…块如下:<?xml…...

Kali/Debian Linux 安装Docker Engine

0x01 卸载旧版本 在安装Docker Engine之前&#xff0c;需要卸载已经安装的可能有冲突的软件包。一些维护者在他们的仓库提供的Docker包可能是非Docker官方发行版&#xff0c;须先卸载这些软件包&#xff0c;然后才能安装Docker官方正式发行的Docker Engine版本。 要卸载的软件…...

Spring 应用合并之路(二):峰回路转,柳暗花明 | 京东云技术团队

书接上文&#xff0c;前面在 [Spring 应用合并之路&#xff08;一&#xff09;&#xff1a;摸石头过河]介绍了几种不成功的经验&#xff0c;下面继续折腾… 四、仓库合并&#xff0c;独立容器 在经历了上面的尝试&#xff0c;在同事为啥不搞两个独立的容器提醒下&#xff0c;…...

SQL Error 1366, SQLState HY000

SQL错误 1366 和 SQLState HY000 通常指的是 MySQL 与字符编码或数据截断有关的问题。当尝试将数据插入具有与正在插入的数据不兼容的字符集或排序规则的列时&#xff0c;或者正在插入的数据对于列来说过长时&#xff0c;就会出现此错误。 解决方式&#xff1a; 检查列长度&am…...

Codeforces Round 893 (Div. 2)(VP-7,寒假加训)

VP时间 A. 关键在于按c的按钮 c&1 Alice可以多按一次c按钮 也就是a多一个&#xff08;a&#xff09; 之后比较a,b大小即可 !(c&1) Alice Bob操作c按钮次数一样 1.ac B.贪心 一开始会吃饼干 如果有卖饼的就吃 如果隔离一段时间到d没吃就吃&#xff08;当时…...

MySQL第四战:视图以及常见面试题(上)

目录 目录&#xff1a; 一.视图 1.介绍什么是视图 2.视图的语法 语法讲解 实例操作 二.MySQL面试题 1.SQL脚本 2.面试题实战 三.思维导图 目录&#xff1a; 随着数字化时代的飞速发展&#xff0c;数据库技术&#xff0c;特别是MySQL&#xff0c;已经成为IT领域中不可…...

C语言程序设计——程序流程控制方法(一)

C语言关系运算符 ---等于ab!不等于a!b<、>小于和大于a>b 、a<b<、>小于等于、大于等于a>b 、a<b!非!(0)、!(NULL) 在C99之后&#xff0c;C语言开始支持布尔类型&#xff0c;头文件是stdbool.h。在文中我所演示的所有代码均是C99版。 在C语言上上述关…...

手把手教你实现glitch free的时钟切换电路(附Verilog代码)

手把手教你实现glitch free的时钟切换电路&#xff08;附Verilog代码&#xff09; 时钟切换电路是数字系统设计中的关键模块&#xff0c;尤其在多时钟域系统中&#xff0c;可靠的时钟切换能确保系统稳定运行。本文将深入探讨如何实现无毛刺&#xff08;glitch free&#xff09;…...

Windows系统OpenClaw避坑指南:nanobot镜像部署常见报错解决

Windows系统OpenClaw避坑指南&#xff1a;nanobot镜像部署常见报错解决 1. 为什么选择nanobot镜像部署OpenClaw 去年我在尝试将OpenClaw接入本地大模型时&#xff0c;被复杂的依赖关系和GPU配置折磨得够呛。直到发现星图平台的nanobot镜像——这个预装了Qwen3-4B-Instruct模型…...

从CPU指令到C++代码:拆解 std::atomic fetch_add 在 x86 和 ARM 平台上的底层实现与性能差异

从CPU指令到C代码&#xff1a;拆解 std::atomic fetch_add 在 x86 和 ARM 平台上的底层实现与性能差异 在现代高性能并发编程中&#xff0c;原子操作是构建无锁数据结构和线程安全代码的基石。std::atomic 的 fetch_add 操作看似简单&#xff0c;但其底层实现却因硬件架构差异而…...

OpenClaw+Qwen3-VL:30B:低成本搭建飞书多模态机器人

OpenClawQwen3-VL:30B&#xff1a;低成本搭建飞书多模态机器人 1. 为什么选择本地部署多模态助手&#xff1f; 去年我在团队内部尝试用商业API搭建了一个飞书机器人&#xff0c;用于处理日常的图片识别和文档分析需求。三个月后收到账单时&#xff0c;发现仅图片识别这一项功…...

百川2-13B-4bits模型商用指南:OpenClaw自动化服务合规部署要点

百川2-13B-4bits模型商用指南&#xff1a;OpenClaw自动化服务合规部署要点 1. 商用授权与合规基础 百川2-13B-4bits模型作为国内少数明确开放商用申请的大语言模型&#xff0c;其授权体系与常见的开源协议有本质区别。我在实际部署过程中发现&#xff0c;很多开发者容易忽略一…...

【Unity实战】利用Preserve特性解决代码裁剪导致的反射调用失效问题

1. 代码裁剪与反射调用的相爱相杀 第一次遇到这个问题是在去年做手游项目的时候。那天测试同事急匆匆跑过来说&#xff1a;"哥&#xff0c;安卓包加载存档直接闪退&#xff01;"我心想编辑器里明明好好的&#xff0c;怎么打包就出问题&#xff1f;打开日志一看&#…...

Paste 轻量级剪贴板管理工具使用指南

Paste 轻量级剪贴板管理工具使用指南 【免费下载链接】paste A no-datastore, client-side paste service. 项目地址: https://gitcode.com/gh_mirrors/past/paste 一、场景化导入&#xff1a;当剪贴板成为你的效率瓶颈 想象一下这样的工作场景&#xff1a;你正在整理一…...

模型介导钓鱼:AI 助手被诱导生成钓鱼内容的机理与防御

摘要 随着 Microsoft 365 Copilot、Google Gemini for Workspace 等 AI 助手在企业办公场景的深度普及&#xff0c;一类依托提示注入实现的模型介导钓鱼&#xff08;Model-Mediated Phishing&#xff09; 攻击快速兴起。攻击者通过在正常邮件中嵌入低可见性恶意指令&#xff0c…...

OS17.【Linux】进程基础知识(1)

目录 1.浅层定义 程序和进程的区别 2.查看进程的方法 ps ajx top 查看/proc目录 ​编辑 PID 3.手动用ps查看自己运行的程序 在/proc手动查看自己运行的程序目录 杀死进程的常用方法 进程目录中的文件 cwd 理解"当前路径"的含义 4.如何管理一个进程 程…...

STM32F103C6 USB DFU升级实战:从CubeMX配置到DfuSeDemo烧录,一步步教你搞定Bootloader设计

STM32F103C6 USB DFU升级全流程解析&#xff1a;从硬件配置到安全跳转的深度实践 在嵌入式开发中&#xff0c;固件升级是产品生命周期中不可或缺的环节。想象一下这样的场景&#xff1a;你的设备已经部署在客户现场&#xff0c;突然发现一个需要紧急修复的BUG&#xff0c;或者需…...