个人笔记---关于详解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.rhkb.cn/news/13603.html

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

相关文章

Fedora 的 2025 年展望:AI 集成与 HDR 支持打造强大 Linux 桌面体验

Fedora 项目已经从节庆活动中恢复&#xff0c;准备在未来几个月推出一系列关键计划。Red Hat 软件工程总监 Christian Schaller 在他的博客文章中分享了 2025 年 Fedora 发行版的重点发展方向和优先事项&#xff0c;涵盖了人工智能集成、Wayland、HDR 协议、PipeWire 等多个领域…

java时间相关类

时间相关类 JDK7以前时间相关类Date时间时间换算 SimpleDateFormat格式化时间作用格式化时间常用模式对应关系 Calendar日历作用Calendar常用方法 JDK8新增时间相关类Date类ZoneId常用方法 Instant时间戳常见方法 ZoneDateTime带时区的时间常用方法 日期格式化类常用方法 日历类…

js-对象-JSON

JavaScript自定义对象 JSON 概念: JavaScript Object Notation&#xff0c;JavaScript对象标记法. JSON 是通过JavaScript 对象标记法书写的文本。 由于其语法简单&#xff0c;层次结构鲜明&#xff0c;现多用于作为数据载体&#xff0c;在网络中进行数据传输. json中属性名(k…

深入理解 Java 接口的回调机制 【学术会议-2025年人工智能与计算智能(AICI 2025)】

大会官网&#xff1a;www.icaici.org 前言 回调是一种非常重要的编程技术&#xff0c;它广泛应用于事件驱动的编程、异步任务和框架设计中。在 Java 中&#xff0c;回调机制通常通过 接口 来实现。本篇博客将详细解析 Java 接口的回调原理、实现方式&#xff0c;以及实际开发中…

import { Component, Vue, Prop, Watch } from ‘vue-property-decorator‘

文章目录 导入部分的解释总结Vue 3 的推荐替代方案总结 你提供的代码片段是使用 vue-property-decorator 库的示例&#xff0c;这是一个第三方库&#xff0c;它提供了 Vue 组件的装饰器&#xff0c;使得编写类风格的 Vue 组件更加方便。以下是对代码中每个部分的详细解释&…

【Kubernetes Pod间通信-第3篇】Kubernetes中Pod与ClusterIP服务之间的通信

引言 我们之前了解了在不同场景下,Kubernetes中Pod之间的通信是如何路由的。 【Kubernetes Pod间通信-第1篇】在单个子网中使用underlay网络实现Pod到Pod的通信【Kubernetes Pod间通信-第2篇】使用BGP实现Pod到Pod的通信现在,我们来看看在集群中,Pod与服务之间的通信是如何…

el-table中的某个字段最多显示两行,超出部分显示“...详情”,怎么办

文章目录 背景需求需求分析 解决方案在线体验灵感来源我的实现方案 总结 背景 需求 比如&#xff0c;有如下一个表格&#xff0c;请你实现它&#xff1a; 要求&#xff1a; 最多显示两行超出部分显示为&#xff1a;“…详情”点击详情&#xff0c;展开全部内容 说明&#x…

如何获取sql数据中时间的月份、年份(类型为date)

可用自带的函数month来实现 如&#xff1a; 创建表及插入数据&#xff1a; create table test (id int,begindate datetime) insert into test values (1,2015-01-01) insert into test values (2,2015-02-01) 执行sql语句,获取月份&#xff1a; select MONTH(begindate)…

数据库系统概论的第六版与第五版的区别,附pdf

我用夸克网盘分享了「数据库系统概论第五六版资源」&#xff0c;点击链接即可保存。 链接&#xff1a;https://pan.quark.cn/s/21a278378dee 第6版教材修订的主要内容 为了保持科学性、先进性和实用性&#xff0c;在第5版教材基础上对全书内容进行了修改、更新和充实。 在科…

告别手动操作!用Ansible user模块高效管理 Linux账户

在企业运维环境中&#xff0c;服务器的用户管理是一项基础但非常重要的任务。比如&#xff0c;当有新员工加入时&#xff0c;我们需要在多台服务器上为他们创建账户并分配合适的权限。而当员工离职或岗位发生变化时&#xff0c;我们也需要迅速禁用或删除他们的账户&#xff0c;…

区块链项目孵化与包装设计:从概念到市场的全流程指南

区块链技术的快速发展催生了大量创新项目&#xff0c;但如何将一个区块链项目从概念孵化成市场认可的产品&#xff0c;是许多团队面临的挑战。本文将从孵化策略、包装设计和市场落地三个维度&#xff0c;为你解析区块链项目成功的关键步骤。 一、区块链项目孵化的核心要素 明确…

51单片机 02 独立按键

一、独立按键控制LED亮灭 轻触按键&#xff1a;相当于是一种电子开关&#xff0c;按下时开关接通&#xff0c;松开时开关断开&#xff0c;实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开。 #include <STC89C5xRC.H> void main() { // P20xFE;while(1){…

免费windows pdf编辑工具

Epdf&#xff08;完全免费&#xff09; 作者&#xff1a;不染心 时间&#xff1a;2025/2/6 Github: https://github.com/dog-tired/Epdf Epdf Epdf 是一款使用 Rust 编写的 PDF 编辑器&#xff0c;目前仍在开发中。它提供了一系列实用的命令行选项&#xff0c;方便用户对 PDF …

【配置环境】VS Code中JavaScript环境搭建

一&#xff0c;环境 Windows 11 家庭中文版&#xff0c;64 位操作系统, 基于 x64 的处理器VS Code 版本: 1.83.1 (user setup)Node.js 版本&#xff1a;20.9.0 二&#xff0c;为什么搭建JavaScript环境 因为在看《重构改善既有代码的设计第2版》的时候&#xff0c;书中的代码展…

结构体排序 C++ 蓝桥杯

成绩排序 #include<iostream> #include<algorithm> using namespace std; struct stu {string name;//名字int grade;//成绩 }; stu a[30]; bool cmp(stu l, stu r) {if (l.grade ! r.grade) return l.grade > r.grade;return l.name < r.name; } int main()…

低代码提升交付效率的公式计算

低&#xff08;无&#xff09;代码平台&#xff08;后统称“低代码”&#xff09;能够提升数字化应用建设、交付效率&#xff0c;已经成为IT从业人员的共识。目前&#xff0c;大部分CIO/CDO都能清晰定位和认知低代码的特点和作用。但仍然有人认为&#xff0c;使用了低代码工具软…

【漫画机器学习】083.安斯库姆四重奏(Anscombe‘s Quartet)

安斯库姆四重奏&#xff08;Anscombes Quartet&#xff09; 1. 什么是安斯库姆四重奏&#xff1f; 安斯库姆四重奏&#xff08;Anscombes Quartet&#xff09;是一组由统计学家弗朗西斯安斯库姆&#xff08;Francis Anscombe&#xff09; 在 1973 年 提出的 四组数据集。它们…

【PDF多区域识别】如何批量PDF指定多个区域识别改名,基于Windows自带的UWP的文字识别实现方案

海关在对进口货物进行查验时,需要核对报关单上的各项信息。对报关单 PDF 批量指定区域识别改名后,海关工作人员可以更高效地从文件名中获取关键信息,如货物来源地、申报价值等。例如文件名 “[原产国]_[申报价值].pdf”,有助于海关快速筛选重点查验对象,提高查验效率和监管…

C基础寒假练习(6)

一、终端输入行数&#xff0c;打印倒金字塔 #include <stdio.h> int main() {int rows;printf("请输入倒金字塔的行数: ");scanf("%d", &rows);for (int i rows; i > 0; i--) {// 打印空格for (int j 0; j < rows - i; j) {printf(&qu…

使用 CSS 实现透明效果

在 CSS 中&#xff0c;实现透明效果有几种方法&#xff0c;具体使用哪种方法取决于具体需求。以下是一些常见的方法&#xff1a; 使用 opacity 属性&#xff1a; opacity 属性可以设置整个元素的透明度&#xff0c;包括其所有的子元素。 .transparent { opacity: 0.5; /* 0 表…