个人笔记---关于详解threadlocal 上下文环境存储的最佳数据类型

个人原因很久没有写代码,对于一些基础的数据类型有一些忘记,可以根据gpt和我当时的问答进行复习

关于拦截器,由于在请求的到达controller处理器之前,拦截器(当然过滤器也可以实现,我感觉都差不多)就把上下文设置在了线程副本中,那么这个请求到处理器的这些代码进行查询出来的上下文也就是这个拦截器设置的上下文咯?这样就百分百线程隔离不会租户混乱哈,然后拦截器中有一个处理请求完毕后删除这个线程副本的方法,来删除这个请求的所属租户是吧,.理论上是线程隔离安全的,但是我害怕多个请求同时经过拦截器导致设置的租户上下文是藏数据,但是我又记得ThreadLocal对于每个访问的线程都是一个副本百分百线程的隔离,所以无需担心,这部分我有一些忘记了,详解一下

你的理解基本是对的!但我们来 详细分析一下 ThreadLocal 的线程隔离特性,确保你的租户上下文不会混乱,并解答你关于并发访问的担忧。


1. ThreadLocal 的线程隔离原理

ThreadLocal每个线程的私有变量存储,它的本质是 每个线程都有自己独立的副本,不会影响其他线程。其核心机制:

  • ThreadLocal 的值存储在 Thread 对象的 threadLocals 变量中,而不是 ThreadLocal 本身。
  • 每个线程访问 ThreadLocal 时,都会查找自己线程的 threadLocals,不会干扰其他线程的数据。
  • 不同线程访问相同的 ThreadLocal 变量时,各自的数据是独立的,不会互相影响。

所以:
不会出现多个请求之间的租户数据混乱
即使多个请求并发执行,ThreadLocal 仍然是线程安全的


2. 你的 拦截器 逻辑分析

由于 拦截器在请求到达 Controller 之前,就已经设置了 ThreadLocal 变量,那么后续 ServiceMapper 访问时,就能读取这个 ThreadLocal 变量中的租户信息。

流程
  1. 请求到达拦截器

    • 解析租户 ID(从请求头、JWT、URL 参数等获取)
    • 设置 ThreadLocal 保存租户 ID
  2. 请求进入 Controller -> Service -> Mapper

    • ServiceMapper 查询时,直接从 ThreadLocal 获取租户 ID,保证 SQL 查询时是隔离的。
  3. 请求处理完成

    • 拦截器的 afterCompletion() 方法执行,清理 ThreadLocal 变量
    • 释放租户数据,防止 线程池复用时数据泄露
代码示例
@Component
public class TenantInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String tenantIdStr = request.getHeader("X-Tenant-Id"); // 从请求头获取租户 IDif (tenantIdStr != null) {TenantContext.setCurrentTenant(Long.parseLong(tenantIdStr));}return true; // 继续执行后续请求}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {TenantContext.clear(); // 清除 `ThreadLocal`,防止线程复用导致数据泄露}
}

3. 可能的多线程问题

尽管 ThreadLocal 在单个请求的线程中是安全的,但有 以下两种情况可能导致数据混乱

(1) 线程池复用问题

Spring MVC 默认使用线程池,请求处理完后线程不会立即销毁,而是会被 回收到线程池。如果不清理 ThreadLocal,下一个请求可能会 复用上一个请求的租户数据,导致租户数据污染。

已在 afterCompletion() 里调用 TenantContext.clear() 解决这个问题!


(2) 多线程异步调用问题

如果你的代码在 ControllerService 里使用了 @Async 或者 CompletableFuture 来创建 新的线程,那么这个新线程不会继承 ThreadLocal 变量,导致租户数据丢失。

解决方案

如果你在 @Async 方法里也需要租户信息,你需要 手动传递 tenantId,或者使用 InheritableThreadLocal

手动传递 tenantId:

@Async
public void asyncTask(Long tenantId) {TenantContext.setCurrentTenant(tenantId);try {// 执行异步任务} finally {TenantContext.clear();}
}

使用 InheritableThreadLocal 让子线程继承主线程的租户信息:

public class TenantContext {private static final InheritableThreadLocal<Long> CURRENT_TENANT = new InheritableThreadLocal<>();public static void setCurrentTenant(Long tenantId) {CURRENT_TENANT.set(tenantId);}public static Long getCurrentTenant() {return CURRENT_TENANT.get();}public static void clear() {CURRENT_TENANT.remove();}
}

InheritableThreadLocal 允许子线程 继承 父线程的 ThreadLocal 数据,适用于 @Async 场景。


4. 总结

ThreadLocal 是线程隔离的,不会导致多个请求的数据混乱
afterCompletion() 里清理 ThreadLocal,防止线程池复用污染数据
异步线程不会自动继承 ThreadLocal,需要手动传递租户 ID 或使用 InheritableThreadLocal

这样,你的 多租户实现就是安全的!🚀

threadlocal 就相当于一个副本地址 当线程访问这个上下文的数据,数据并不在threadlocal这个对象中,而是在访问的线程实列自己身上,那么给我的感觉就像是数据拷贝的地址?

是的,你的理解很接近!但更准确地说,ThreadLocal 并不存储数据本身,而是为每个线程创建一个独立的数据副本,并将这个数据存储在线程的 Thread 实例中。它的本质并不是数据拷贝,而是 线程独享的存储映射


1. ThreadLocal 的存储结构

(1) ThreadLocal 本身不存储数据
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

这段代码 只是创建了一个 ThreadLocal 变量,并不会真正存储数据

(2) 每个线程的 Thread 实例内部维护着 ThreadLocal 的数据

每个 Thread 对象内部都有一个 ThreadLocalMap

public class Thread {// 线程内部维护一个 ThreadLocalMapThreadLocal.ThreadLocalMap threadLocals = null;
}

当调用 threadLocal.set("data") 时:

  • ThreadLocal 并不会存数据,它只是 作为 key
  • 真实的数据 存储在 ThreadLocalMap,并且这个 ThreadLocalMap 归属于当前 Thread

示意图

线程 A:  [ThreadLocalMap] -> { ThreadLocal1 -> "数据A" }
线程 B:  [ThreadLocalMap] -> { ThreadLocal1 -> "数据B" }
线程 C:  [ThreadLocalMap] -> { ThreadLocal1 -> "数据C" }

每个线程的数据都是 独立存储的,不会互相影响


2. ThreadLocal 的数据访问机制

ThreadLocal 调用 get() 方法时:

  1. 先获取当前线程 Thread.currentThread()
  2. 从该线程的 ThreadLocalMap 中取出 ThreadLocal 关联的数据
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMapif (map != null) {ThreadLocalMap.Entry e = map.get(this); // 以当前 ThreadLocal 作为 key 取值if (e != null) {return (T) e.value;}}return null;
}

所以,你说的 “数据并不在 ThreadLocal 对象中,而是在访问的线程实例上” 这个理解是对的!👍


3. 为什么 ThreadLocal 是线程隔离的?

  • 每个线程都拥有自己的 ThreadLocalMap
  • 数据存储在 ThreadLocalMap 里,而不是 ThreadLocal 本身
  • 不同线程访问 ThreadLocal 时,只能访问自己线程的 ThreadLocalMap
  • 不同线程的数据不会互相污染

因此,ThreadLocal 可以用来存储每个线程的租户 ID,保证租户数据不会串线程!


4. ThreadLocal 只是存了“地址”吗?

可以这么理解,ThreadLocal 本质上是一个 key,而数据是存储在 ThreadLocalMap 里的。但它不是简单的地址拷贝,而是 在每个线程的 ThreadLocalMap 中存储了一个独立的副本

你可以把它理解成:

每个线程都有一个独立的小型数据仓库,ThreadLocal 只是仓库的门牌号,而真正的数据在仓库里。


5. 你关心的并发问题

由于每个线程都有自己的 ThreadLocalMap

  • 线程 A 访问 ThreadLocal 时,只能获取自己 ThreadLocalMap 里的数据
  • 线程 B 访问 ThreadLocal 时,只能获取自己 ThreadLocalMap 里的数据
  • 线程 C 访问 ThreadLocal 时,只能获取自己 ThreadLocalMap 里的数据

所以:
线程之间互不影响,线程 A 无法访问线程 B 的数据
即使多个请求并发执行,每个请求的租户数据都是独立的,不会串数据


6. 需要注意的问题

(1) 线程池问题

如果使用 线程池,线程不会被销毁,而是会被复用。如果不清理 ThreadLocal,下一个请求可能会复用上一个线程的租户数据,导致数据污染。

解决方案:在 afterCompletion() 里清理 ThreadLocal

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {TenantContext.clear();
}
(2) 异步线程问题

ThreadLocal 不会自动传播到子线程,如果 @Async 方法需要租户数据,必须手动传递。

解决方案 1:手动传递租户 ID

@Async
public void asyncTask(Long tenantId) {TenantContext.setCurrentTenant(tenantId);try {// 执行异步任务} finally {TenantContext.clear();}
}

解决方案 2:使用 InheritableThreadLocal 让子线程继承主线程数据

public class TenantContext {private static final InheritableThreadLocal<Long> CURRENT_TENANT = new InheritableThreadLocal<>();public static void setCurrentTenant(Long tenantId) {CURRENT_TENANT.set(tenantId);}public static Long getCurrentTenant() {return CURRENT_TENANT.get();}public static void clear() {CURRENT_TENANT.remove();}
}

7. 总结

ThreadLocal 不是存数据的,而是一个 key,每个线程的 ThreadLocalMap 里存储数据副本
ThreadLocal 保证了线程隔离,不会导致多个请求的数据串掉
线程池复用时要 clear(),否则会数据污染
异步线程不会继承 ThreadLocal,需要手动传递或用 InheritableThreadLocal

你现在应该完全理解 ThreadLocal 线程隔离的本质 了吧!🚀

WEB框架的本质

所以threadlocal作为上下文的选择也就体现出来了,对于web框架基本都是对于http请求的链式处理,springboot框架也是,当有一个请求进入处理器controller时候时,ioc会独立一个线程出来进行处理,而这些上文存储的数据也是在这个线程内的,所以这样达到了数据隔离的效果,web框架大多都是这么实现的,比如go的context,和gin这些框架,和python的flask?

完全正确!🎯

Spring Boot 这样的 Web 框架里,每个 HTTP 请求都会由一个独立的线程处理(默认是 Tomcat 的线程池提供的 Executor),而 ThreadLocal 绑定的数据就是 请求范围内独立的上下文,确保数据隔离。

你提到的 Go 的 context 和 Gin、Python 的 Flask 也有类似的机制,虽然它们不使用 ThreadLocal,但它们的请求上下文处理思路是类似的:


1. Java (Spring Boot) - ThreadLocal 方式

Spring 的 Interceptor 中使用 ThreadLocal 作为请求范围的数据存储:

public class TenantInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String tenantId = request.getHeader("X-Tenant-ID");TenantContext.setTenant(tenantId); // 存入 ThreadLocalreturn true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {TenantContext.clear(); // 清理,防止线程池复用时污染数据}
}

这样,每个请求都有独立的 ThreadLocal 上下文,即使高并发请求,也不会串租户数据。


2. Go (Gin) - context.Context 方式

Go 由于 Goroutine 复用较多,不会使用 ThreadLocal,而是用 context.Context 显式传递上下文:

func TenantMiddleware() gin.HandlerFunc {return func(c *gin.Context) {tenantID := c.GetHeader("X-Tenant-ID")ctx := context.WithValue(c.Request.Context(), "tenantID", tenantID)c.Request = c.Request.WithContext(ctx)c.Next() // 继续处理}
}

Go 语言不会绑定数据到 Goroutine,而是 显式传递 context.Context,这样即使 Goroutine 被复用,上下文数据仍然正确。


3. Python (Flask) - g 全局对象

Flask 由于 是单线程处理请求,不需要 ThreadLocal,而是使用 flask.g 作为请求范围的数据存储

from flask import Flask, g, requestapp = Flask(__name__)@app.before_request
def before_request():g.tenant_id = request.headers.get("X-Tenant-ID")  # 存储租户信息@app.route("/")
def index():return f"Current Tenant: {g.tenant_id}"  # 线程安全if __name__ == "__main__":app.run()

Flask 处理请求时,每个请求都有一个独立的 g,保证不会数据串联。


总结

语言Web 框架上下文存储方式线程隔离
JavaSpring BootThreadLocal线程独立,绑定线程局部变量
GoGincontext.Context显式传递,避免 Goroutine 复用问题
PythonFlaskflask.g线程局部对象,每个请求独立

🚀 不同语言的 Web 框架处理方式不同,但核心目标都是保证请求范围内的数据隔离,防止数据串行和污染!

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

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

相关文章

DeepSeek 部署过程中的问题

文章目录 DeepSeek 部署过程中的问题一、部署扩展&#xff1a;docker 部署 DS1.1 部署1.2 可视化 二、问题三、GPU 设置3.1 ollama GPU 的支持情况3.2 更新 GPU 驱动3.3 安装 cuda3.4 下载 cuDNN3.5 配置环境变量 四、测试 DeepSeek 部署过程中的问题 Windows 中 利用 ollama 来…

基础算法——二维前缀和

二维前缀和 我们先前已经了解了前缀和思想&#xff0c;二维前缀和感觉上就是一维前缀和的进阶&#xff0c;下面 &#xff0c;我们剖析一下两种前缀和。 一维前缀和 一维前缀和的核心就是这两个公式&#xff0c;二维前缀和也差不多的嘞 下面我们来推理一下二维前缀和 已知&a…

每日Attention学习19——Convolutional Multi-Focal Attention

每日Attention学习19——Convolutional Multi-Focal Attention 模块出处 [ICLR 25 Submission] [link] UltraLightUNet: Rethinking U-shaped Network with Multi-kernel Lightweight Convolutions for Medical Image Segmentation 模块名称 Convolutional Multi-Focal Atte…

2. K8S集群架构及主机准备

本次集群部署主机分布K8S集群主机配置主机静态IP设置主机名解析ipvs管理工具安装及模块加载主机系统升级主机间免密登录配置主机基础配置完后最好做个快照备份 2台负载均衡器 Haproxy高可用keepalived3台k8s master节点5台工作节点(至少2及以上)本次集群部署主机分布 K8S集群主…

游戏引擎学习第89天

回顾 由于一直没有渲染器&#xff0c;终于决定开始动手做一个渲染器&#xff0c;虽然开始时并不确定该如何进行&#xff0c;但一旦开始做&#xff0c;发现这其实是正确的决定。因此&#xff0c;接下来可能会花一到两周的时间来编写渲染器&#xff0c;甚至可能更长时间&#xf…

链式结构二叉树(递归暴力美学)

文章目录 1. 链式结构二叉树1.1 二叉树创建 2. 前中后序遍历2.1 遍历规则2.2 代码实现图文理解 3. 结点个数以及高度等二叉树结点个数正确做法&#xff1a; 4. 层序遍历5. 判断是否完全二叉树 1. 链式结构二叉树 完成了顺序结构二叉树的代码实现&#xff0c;可以知道其底层结构…

Kubernetes 中 BGP 与二层网络的较量:究竟孰轻孰重?

如果你曾搭建过Kubernetes集群,就会知道网络配置是一个很容易让人深陷其中的领域。在负载均衡器、服务通告和IP管理之间,你要同时应对许多变动的因素。对于许多配置而言,使用二层(L2)网络就完全能满足需求。但边界网关协议(BGP)—— 支撑互联网运行的技术 —— 也逐渐出…

Linux提权--John碰撞密码提权

​John the Ripper​&#xff08;简称 John&#xff09;是一个常用的密码破解工具&#xff0c;可以通过暴力破解、字典攻击、规则攻击等方式&#xff0c;尝试猜解用户密码。密码的弱度是提权攻击中的一个重要因素&#xff0c;如果某个用户的密码非常简单或是默认密码&#xff0…

大数据学习之Spark分布式计算框架RDD、内核进阶

一.RDD 28.RDD_为什么需要RDD 29.RDD_定义 30.RDD_五大特性总述 31.RDD_五大特性1 32.RDD_五大特性2 33.RDD_五大特性3 34.RDD_五大特性4 35.RDD_五大特性5 36.RDD_五大特性总结 37.RDD_创建概述 38.RDD_并行化创建 演示代码&#xff1a; // 获取当前 RDD 的分区数 Since ( …

[创业之路-286]:《产品开发管理-方法.流程.工具 》-1- IPD两个跨职能团队的组织

IPD&#xff08;集成产品开发&#xff09;中的两个重要跨职能组织是IPMT&#xff08;集成产品管理团队&#xff09;和PDT&#xff08;产品开发团队&#xff09;。 在IPD&#xff08;集成产品开发&#xff09;体系中&#xff0c;IRB&#xff08;投资评审委员会&#xff09;、IPM…

DeepSeek 提示词之角色扮演的使用技巧

老六哥的小提示&#xff1a;我们可能不会被AI轻易淘汰&#xff0c;但是会被“会使用AI的人”淘汰。 在DeepSeek的官方提示库中&#xff0c;有“角色扮演&#xff08;自定义人设&#xff09;”的提示词案例。截图如下&#xff1a; 在“角色扮演”的提示词案例中&#xff0c;其实…

第二个Qt开发实例:在Qt中利用GPIO子系统和sysfs伪文件系统实现按钮(Push Button)点击控制GPIO口(效果为LED2灯的灭和亮)

引言 本文承接博文 https://blog.csdn.net/wenhao_ir/article/details/145420998 里的代码&#xff0c;在那里面代码的基础上添加上利用sysfs伪文件系统实现按钮(Push Button)点击控制GPIO口的代码&#xff0c;进而实现LED2灯的灭和亮。 最终的效果是点击下面的LED按钮实现LED…

登山第十七梯:矩形拟合——无惧噪声

文章目录 一 摘要 二 资源 三 内容 (文章末尾提供源代码) 一 摘要 目前,获取点集的矩形拟合结果的主要方法是计算其最小外包直立矩形或者旋转矩形。这些方法简单、易用,在数据质量良好的情况下能够较好的贴合矩形形状。然而,在数据缺失时,最小外包围盒方法将会…

57. Uboot图形化界面配置

一、Uboot图形化配置方法 1、通过终端配置。 2、进入到uboot的源码根目录下。 3、首先默认配置 make mx6ull_alientek_emmc_defconfig //默认配置 4、输入make menuconfig。打开图形化配置界面。 5、注意&#xff0c;新电脑需要安装ncurses库。sudo apt-get install libncurs…

kalman滤波器C++设计仿真实例第三篇

1. 仿真场景 水面上有条船在做匀速直线航行&#xff0c;航行过程中由于风和浪的影响&#xff0c;会有些随机的干扰&#xff0c;也就是会有些随机的加速度作用在船身上&#xff0c;这个随机加速度的均方差大约是0.1&#xff0c;也就是说方差是0.01。船上搭载GPS设备&#xff0c;…

(2025|ICLR,音频 LLM,蒸馏/ALLD,跨模态学习,语音质量评估,MOS)音频 LLM 可作为描述性语音质量评估器

Audio Large Language Models Can Be Descriptive Speech Quality Evaluators 目录 1. 概述 2. 研究背景与动机 3. 方法 3.1 语音质量评估数据集 3.2 ALLD 对齐策略 4. 实验结果分析 4.1 MOS 评分预测&#xff08;数值评估&#xff09; 4.2 迁移能力&#xff08;在不同…

stm32生成hex文件详解

1.产生的map文件干啥的&#xff1f; 2.组成情况&#xff1f;&#xff1f;&#xff1f; 废话少说&#xff0c;直接上代码具体内容况&#xff1a; Component: ARM Compiler 5.06 update 7 (build 960) Tool: armlink [4d3601]Section Cross Referencesstartup_stm32f103xe.o(S…

百度热力图数据获取,原理,处理及论文应用6

目录 0、数据简介0、示例数据1、百度热力图数据日期如何选择1.1、其他实验数据的时间1.2、看日历1.3、看天气 2、百度热力图几天够研究&#xff1f;部分文章统计3、数据原理3.1.1 ** 这个比较重要&#xff0c;后面还会再次出现。核密度的值怎么理解&#xff1f;**3.1.2 Csv->…

[转]Java面试近一个月的面试总结

本文是在学习中的总结&#xff0c;欢迎转载但请注明出处&#xff1a;http://blog.csdn.net/pistolove/article/details/46753275 前言 打算换个工作&#xff0c;近一个月面试了不少的公司&#xff0c;下面将一些面试经验和思考分享给大家。另外校招也快要开始了&#xff0c;为…

学习threejs,tga格式图片文件贴图

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️TGA图片1.2 ☘️THREE.Mesh…