ThreadLocal

一、先用一个“生活化”类比来理解

假设有一个健身房,里面有很多储物柜(Locker)。每次来一个人,就分配给他(或她)一个储物柜,里面可以放这位客人的私人物品(例如手机、衣服、钥匙等)。当另一个人来健身时,也分配另一个不同的储物柜,两个客人之间不会互相影响或混用柜子。

  • 储物柜 = ThreadLocal 中存储的数据
  • 每位客人 = 程序里的“线程”
  • 健身房 = 运行时的 JVM 或者说程序整体环境

对比:

  1. 每个客人只能访问自己的储物柜,也就是说不同客人是不会看、也看不到彼此的储物柜里的东西;
  2. 如果客人走了不把自己的柜子清空,下一次来另一个客人有可能会被分到同一个柜子,发现里面还有遗留物品,产生混乱;
  3. 所以,用完就清空非常关键;
  4. 同时,这个储物柜对于客人来说,在他们使用期间相当于随时可取随时可放的“私有空间”。

把这个类比映射到程序世界,就是:

  • “客人” -> “线程”
  • “储物柜” -> “线程本地变量 (Thread-Local variable)”
  • 每个线程操作的那份数据是独立且私有的,不与其他线程冲突。

这便是 ThreadLocal 最本质的概念:给每个线程分配一个私有的存储空间,使得该线程可以在里面读写数据,而其他线程无法干涉。


二、什么是 ThreadLocal,为什么要有它?

1. “线程局部变量”的来历

在多线程环境中,如果多条线程都要访问(读写)同一个全局变量,就会遇到并发、安全、数据一致性等问题。我们可能需要加锁、加 volatile 等,或者想办法把这个变量变成方法参数层层传递,十分繁琐。

但有些场景,数据其实不需要被线程之间共享,而是“线程私有”的。举例:

  • 当前线程处理的是“请求A”,里面存了“用户ID=1001”;
  • 另一个线程处理“请求B”,里面存了“用户ID=2002”;
  • 这两条线程对 “用户ID” 的值并没有交互或共享的必要,每个线程只关心“自己的用户ID”即可。

如果我们希望快速地在同一个线程的上下文里保存并访问这样的数据,同时不必担心和其他线程的冲突,也避免了在方法参数间反复传递,那么 ThreadLocal 就登场了。

2. ThreadLocal 的核心点

  • 同一个 ThreadLocal 实例在不同线程中,会分别存一份“线程私有的数据”
  • 线程之间互不影响,也互不可见。
  • 这在多层调用、跨模块时,非常方便,省得层层传递或维护公共状态。

三、ThreadLocal 的内部原理

如果你对底层实现不感兴趣,可以跳过,但了解一下有助于理解“为什么一定要及时清理”。

1. 每个线程有一个 ThreadLocalMap

在 Java 的实现中,每一个 Thread(准确说是 java.lang.Thread 对象)内部,都会有一个 ThreadLocalMap 的属性。它是一个散列表结构,用来存储 <ThreadLocal<?>, Object> 这样的键值对。

  • 当我们对某个 ThreadLocal 实例调用 set(value) 时,实际操作的是
    当前线程(Thread.currentThread())内部的 ThreadLocalMap ,往那张表里塞入一条记录:key = 该 ThreadLocal 对象,value = value

  • 当我们对同一个 ThreadLocal 实例调用 get() 时,它会去当前线程ThreadLocalMap 里找 key=这个 ThreadLocal 的记录,然后把 value 取出来返回给我们。

所以,每个线程都维护着一张自己的 ThreadLocalMap,里面可能会存多条记录。不同线程各自一张表,所以存储在其中的数据自然是互不可见的。

2. “key 为弱引用” 及“需要手动 remove()”

ThreadLocalMap 有一个特殊处理:它对 key(即 ThreadLocal 对象)使用“弱引用(WeakReference)”来避免内存泄露。但如果 ThreadLocal 对象被垃圾回收了,而我们忘记调用 remove() 去清理 Map 里的 value,那么这个 value 可能会变成”Key = null, Value = XXX“ 的悬挂条目(zombie entry),从而导致内存无法被回收,产生内存泄漏

因此,官方建议:在使用完 ThreadLocal 后,显式调用 remove() 方法,以保证我们在后续不会出现残留数据,也更安全。


四、怎么使用 ThreadLocal?(代码演示)

我们先写一个最简单的用法:演示如何给各个线程设置不同的值,并取出来:

public class ThreadLocalExample {// 声明一个静态 ThreadLocal 用来存储 Stringprivate static final ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {// 启动两个线程Thread t1 = new Thread(() -> {// 设置当前线程的局部变量threadLocal.set("数据A - 来自线程T1");// 获取并打印System.out.println("T1得到: " + threadLocal.get());// 用完后,清理threadLocal.remove();});Thread t2 = new Thread(() -> {threadLocal.set("数据B - 来自线程T2");System.out.println("T2得到: " + threadLocal.get());threadLocal.remove();});t1.start();t2.start();}
}

典型输出会是:

T1得到: 数据A - 来自线程T1
T2得到: 数据B - 来自线程T2

你会看到,“T1”和“T2”各自 set 的值不会相互影响。

一些可选用法

  • get():如果从未 set() 过,默认返回 null
  • set(T value):把当前线程的值设为 value,类型是你定义的泛型。
  • remove():清理当前线程的这份值,非常重要

可以把 ThreadLocal 当作线程范围的存储容器。在多层调用或框架中,你不想在每个方法都增加一个形参来传递某个信息(比如 UserId),那就把它放进 ThreadLocal,在需要的地方 get() 出来就行。


五、在 Spring Boot / Web 应用中:为什么这么常见?

1. 场景:一次 HTTP 请求对应一个工作线程

在很多后端 Web 框架(包括 Spring Boot, Tomcat 容器)中,请求进来后通常会被分配到线程池中的某个工作线程去处理。请求处理完再归还线程到池里。

在整个处理过程中(Controller -> Service -> DAO -> ...),都处于同一个线程上下文。这时,ThreadLocal 就特别有用。比如:

(1)存储“当前登录用户ID”

  • 在请求开始时,通过过滤器/拦截器解析出 token,得到 userId;
  • UserContextHolder.setUserId(userId)(内部就是用 ThreadLocal 存储)
  • 后面的 Service、DAO 想获取当前用户ID时,随时 UserContextHolder.getUserId() 即可。
  • 最后在请求结束时 UserContextHolder.remove() 及时清理。

(2)存储“TraceId / RequestId” 以便日志关联

  • 日志系统常用 ThreadLocal 来注入一个“traceId”,这样在一条完整请求的日志里就能输出统一的追踪 ID。
  • 此外,像 Log4j / Logback 的 MDC 也是借助 ThreadLocal 原理来存储信息。

(3)多租户 (Multi-Tenant) 场景

  • 不同租户 ID 需要额外做数据隔离或 SQL 过滤。可以在请求进入时先 ThreadLocal 存一个 tenantId,然后在 DAO 里获取该 tenantId 并做过滤。

2. 好处:避免层层传参 & 线程安全

  • 如果不用 ThreadLocal,我们可能需要在所有的方法调用链里都加一个 Long userId,这样非常麻烦且可读性差;
  • ThreadLocal,我们就“声明一次” -> “随处可取”;又不必担心多线程并发修改的问题,因为它是“当前线程私有”。

六、最佳实践:一定要做到“用完即清除”

1. 为什么必须清除?

  • 线程池环境下,线程是被重复使用的。如果这个线程在上一次请求中存了一个 userId=1001,却没有 remove(), 那下一次有可能处理另一个用户的时候,再 get() 还会拿到残留的 1001,引发严重的安全漏洞或业务错误。
  • 内存泄漏角度,JDK 的实现里,如果 ThreadLocal 对象本身被回收了,而你不清理它在 ThreadLocalMap 中的存储条目,就会有“key=null,但 value 还存活”的情况,造成泄漏。

2. 具体怎么做?

  • 通常在 Web 应用里,可以在过滤器或拦截器finally / afterCompletion() 阶段执行 ThreadLocal.remove()
  • 或者在需要使用的地方,用完就移除,不再保留。

示例(Spring MVC 拦截器):

public class UserContextInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 解析 userIdLong userId = ... // 解析 tokenUserContextHolder.setUserId(userId);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {// 清理UserContextHolder.removeUserId();}
}

UserContextHolder 是一个自己封装的类:

public class UserContextHolder {private static final ThreadLocal<Long> userIdHolder = new ThreadLocal<>();public static void setUserId(Long userId) {userIdHolder.set(userId);}public static Long getUserId() {return userIdHolder.get();}public static void removeUserId() {userIdHolder.remove();}
}
  •  UserContextHolder内部通过创建对应类型的ThreadLocal对象来存储对应类型的线程局部变量。
  • 一定要在在 Web 请求拦截器的 afterCompletion() 里调用ContextHolder中定义的用于清除对应线程局部变量的ThreadLocal的remove方法。

这样就保证了线程不会带着“上一次的私有数据”到下一次请求里,线程安全问题也就迎刃而解。

当有多个线程局部变量 

public class UserContextHolder {// 存储当前线程的用户IDprivate static final ThreadLocal<Long> userIdHolder = new ThreadLocal<>();// 存储当前线程的租户IDprivate static final ThreadLocal<String> tenantIdHolder = new ThreadLocal<>();// 存储当前线程的请求追踪ID(用于日志跟踪)private static final ThreadLocal<String> traceIdHolder = new ThreadLocal<>();// --------------- 用户ID ---------------public static void setUserId(Long userId) {userIdHolder.set(userId);}public static Long getUserId() {return userIdHolder.get();}public static void removeUserId() {userIdHolder.remove();}// --------------- 租户ID ---------------public static void setTenantId(String tenantId) {tenantIdHolder.set(tenantId);}public static String getTenantId() {return tenantIdHolder.get();}public static void removeTenantId() {tenantIdHolder.remove();}// --------------- 追踪ID ---------------public static void setTraceId(String traceId) {traceIdHolder.set(traceId);}public static String getTraceId() {return traceIdHolder.get();}public static void removeTraceId() {traceIdHolder.remove();}// --------------- 统一清理方法,防止内存泄漏 ---------------public static void clear() {userIdHolder.remove();tenantIdHolder.remove();traceIdHolder.remove();}
}
  • 每个变量都单独使用一个 ThreadLocal
    • 这样保证不同的变量互不干扰,每个线程都能存取自己的 userIdtenantIdtraceId
  • 提供 set() / get() / remove() 三个方法
    • setXXX() 用于存储数据
    • getXXX() 用于读取数据
    • removeXXX() 用于清理数据
  • 提供 clear() 方法
    • 一次性清理所有 ThreadLocal 变量,避免在线程池环境下的内存泄漏问题。
    • 在 Web 请求拦截器的 afterCompletion() 里调用 clear(),确保线程不会残留上次请求的数据。

七、总结:ThreadLocal 的本质与注意事项

  • 本质

    • ThreadLocal 为每个线程都“开”了一块独立空间(可以类比“储物柜”);
    • 在这个空间里存的值对其他线程不可见,因此可以保证线程私有数据的安全;
    • 用来避免多线程共享数据的并发问题或者繁琐的参数传递。
  • 使用场景

    • 存储当前请求信息(用户ID、请求ID、租户ID...),日志 MDC,事务上下文,等等。
    • 只在同一个线程内需要访问的数据,不用跨线程。
  • 核心 API

    • set(T value):把当前线程对应的 ThreadLocal 值设置成 value
    • get():获取当前线程对应的值(若没设置过默认 null)。
    • remove():移除当前线程对应的值,必须养成习惯“用完就清理”。
  • 注意事项

    • 一定要保证 remove(),尤其在线程池环境或长期存活线程中,否则会产生数据混乱内存泄漏
    • 切勿将 ThreadLocal 当作跨线程共享的工具,它只适合“线程私有”;
    • 合理封装,可以在一个管理类(例如 UserContextHolder)里完成“set/get/remove”,让代码更清晰。

一句话概括

ThreadLocal 是一个“线程范围的存储工具”,能够在多线程应用中优雅地管理“仅属于当前线程”的数据,用完一定要及时清理,就能避免各种线程安全或内存泄漏问题。


八、再补充几点常见疑问

  • ThreadLocal 与 Synchronized 的区别

    • ThreadLocal 用于“让每个线程各存一份数据”,从根本上避免了数据竞争;
    • synchronized(或锁)用于“让多个线程顺序访问共享数据,防止并发冲突”。
    • 若你根本不需要多线程共享同一个数据,那直接用 ThreadLocal 可能更简洁。
  • 为什么要在 Web 应用尤其注意?

    • 因为 Web 服务器一般都有线程池来处理请求,一个线程在处理完请求A后,可能会被复用去处理请求B。
    • 如果不清理,可能把请求A的东西带给了B,后果严重。
  • ThreadLocalMap 的 key 是弱引用,value 是强引用

    • 这意味着如果 ThreadLocal 实例本身没有被外部引用,就会被 GC 回收,而 ThreadLocalMap 里只剩下 null -> value,value 无法释放,出现内存泄漏。
    • 所以养成 remove() 的好习惯
  • MDC (Mapped Diagnostic Context) 是怎么用 ThreadLocal 的?

    • MDC 其实就是为每个线程(请求)维护一张 Map,存储一些日志上下文,如 traceIduserId 等;
    • 底层就是通过 ThreadLocal<Map<String, String>> 来实现的。

最后,一段话总结

  • ThreadLocal 可以认为是一种在“当前线程”里随时取用的数据存储,不会与其他线程的存储混在一起;
  • 它的出现,大大简化了跨层共享局部数据的开发难度,尤其在Web请求上下文、日志上下文、认证信息等方面非常实用;
  • 但它需要使用者保持极大的自觉记得清理(remove),否则会有副作用(脏数据、内存泄漏);
  • 这是它最常被人诟病的地方,但是如果使用得当,ThreadLocal 是一个非常优雅、简洁的多线程编程“管理器”。

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

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

相关文章

DeepSeek R1 x ApiSmart

根据美国业界的说法&#xff1a;如果一个模型能够在生成良好代码方面表现更出色&#xff0c;那么通常它也能对非代码生成类型的其他用户查询产生更好的答案。 在AI编程领域&#xff0c;市面上已有多款大模型和工具供我们选择。常见的有OpenAI系列模型、Claude 3.5 Sonnet&#…

解决threeJS加载obj gltf和glb模型后颜色太暗的方法

网上找到的部分解决方法 网上找到的部分解决方法 咱们有时候去glb官方下载glb或gltf模型时候&#xff0c;模型显示太黑 其实通过查找后不难发现网上给出了很多解决方法&#xff0c;但是大部分都无法从根本上解决问题。我之前看到有一篇文章对gltf的解决方法是让gltf增加自发光…

GitHub Copilot 越狱漏洞

研究人员发现了两种操控 GitHub 的人工智能&#xff08;AI&#xff09;编码助手 Copilot 的新方法&#xff0c;这使得人们能够绕过安全限制和订阅费用、训练恶意模型等。 第一种技巧是将聊天交互嵌入 Copilot 代码中&#xff0c;利用 AI 的问答能力&#xff0c;使其产生恶意输…

动态规划练习八(01背包问题)

一、问题介绍与解题心得 01背包问题就是每个物品数量只有一个&#xff0c;每个物品可以取或不取&#xff0c;来达到收益最大&#xff0c;或者收益在某个值。 限制条件&#xff1a;背包容量有限&#xff0c;物品个数只有1个 解决问题&#xff1a;从价值入手&#xff08;价值最…

Java实习生面试题汇总

Java实习生面试题汇总 简介 本人是二本大三学生&#xff0c;下半年大四。暑假在上海这边找实习工作&#xff0c;面了几家公司&#xff0c;所问到的问题记录在下面。 因为是在校生&#xff0c;没任何实习经历&#xff0c;一般找我面试的都是小公司&#xff0c;一般问的比较简…

开源安全一站式构建!开启企业开源治理新篇章

在如今信息技术日新月异、飞速发展的数字化时代&#xff0c;开源技术如同一股强劲的东风&#xff0c;为企业创新注入了源源不断的活力&#xff0c;然而&#xff0c;正如一枚硬币有正反两面&#xff0c;开源技术的广泛应用亦伴随着不容忽视的挑战。安全风险如影随形&#xff0c;…

xxl-job 自定义告警短信发送

官方介绍 代码实现 实现 JobAlarm 重写 doAlarm 方法 Component public class SmsJobAlarm implements JobAlarm {Overridepublic boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog) {boolean alarmResult true;// 简单内容&#xff0c;根据业务自行修改String template …

大数据学习之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 ( …

【分布式架构理论3】分布式调用(2):API 网关分析

文章目录 一、API 网关的作用1. 业务层面&#xff1a;简化调用复杂性2. 系统层面&#xff1a;屏蔽客户端调用差异3. 其他方面&#xff1a; 二、API 网关的技术原理1. 协议转换2. 链式处理3. 异步请求机制1. Zuul1&#xff1a;同步阻塞处理2. Zuul2&#xff1a;异步非阻塞处理 三…

3.【BUUCTF】XSS-Lab1

进入题目页面如下 好好好&#xff0c;提示点击图片&#xff0c;点进去页面如下&#xff0c;且url中有传参&#xff0c;有注入点 发现题目给出了源码 查看得到本题的源码 分析一下代码 <!DOCTYPE html><!--STATUS OK--> <!-- 声明文档类型为 HTML5&#xff0c;告…

uniapp小程序自定义中间凸起样式底部tabbar

我自己写的自定义的tabbar效果图 废话少说咱们直接上代码&#xff0c;一步一步来 第一步&#xff1a; 找到根目录下的 pages.json 文件&#xff0c;在 tabBar 中把 custom 设置为 true&#xff0c;默认值是 false。list 中设置自定义的相关信息&#xff0c; pagePath&#x…

105,【5】buuctf web [BJDCTF2020]Easy MD5

进入靶场 先输入试试回显 输入的值成了password的内容 查看源码&#xff0c;尝试得到信息 什么也没得到 抓包&#xff0c;看看请求与响应里有什么信息 响应里得到信息 hint: select * from admin where passwordmd5($pass,true) 此时需要绕过MD5&#xff08;&#xff09;函…

JVM监控和管理工具

基础故障处理工具 jps jps(JVM Process Status Tool)&#xff1a;Java虚拟机进程状态工具 功能 1&#xff1a;列出正在运行的虚拟机进程 2&#xff1a;显示虚拟机执行主类(main()方法所在的类) 3&#xff1a;显示进程ID(PID&#xff0c;Process Identifier) 命令格式 jps […

【大模型】AI 辅助编程操作实战使用详解

目录 一、前言 二、AI 编程介绍 2.1 AI 编程是什么 2.1.1 为什么需要AI辅助编程 2.2 AI 编程主要特点 2.3 AI编程底层核心技术 2.4 AI 编程核心应用场景 三、AI 代码辅助编程解决方案 3.1 AI 大模型平台 3.1.1 AI大模型平台代码生成优缺点 3.2 AI 编码插件 3.3 AI 编…

机器学习--2.多元线性回归

多元线性回归 1、基本概念 1.1、连续值 1.2、离散值 1.3、简单线性回归 1.4、最优解 1.5、多元线性回归 2、正规方程 2.1、最小二乘法 2.2、多元一次方程举例 2.3、矩阵转置公式与求导公式 2.4、推导正规方程0的解 2.5、凸函数判定 成年人最大的自律就是&#xff1a…

2025最新软件测试面试大全(附答案+文档)

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、问&#xff1a;你在测试中发现了一个bug&#xff0c;但是开发经理认为这不是一个bug&#xff0c;你应该怎样解决? 首先&#xff0c;将问题提交到缺陷管理库里…

手写MVVM框架-环境搭建

项目使用 webpack 进行进行构建&#xff0c;初始化步骤如下: 1.创建npm项目执行npm init 一直下一步就行 2.安装webpack、webpack-cli、webpack-dev-server&#xff0c;html-webpack-plugin npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin 3.配置webpac…

如何自定义软件安装路径及Scoop包管理器使用全攻略

如何自定义软件安装路径及Scoop包管理器使用全攻略 一、为什么无法通过WingetUI自定义安装路径&#xff1f; 问题背景&#xff1a; WingetUI是Windows包管理器Winget的图形化工具&#xff0c;但无法直接修改软件的默认安装路径。原因如下&#xff1a; Winget设计限制&#xf…

数据结构实战之线性表(三)

目录 1.顺序表释放 2.顺序表增加空间 3.合并顺序表 4.线性表之链表实现 1.项目结构以及初始代码 2.初始化链表(不带头结点) 3.链表尾部插入数据并显示 4.链表头部插入数据 5.初始化链表&#xff08;带头结点&#xff09; 6.带头结点的链表头部插入数据并显示 7.带头结…

5.6 Mybatis代码生成器Mybatis Generator (MBG)实战详解

文章目录 前言一、Mybatis Generator简介二、Maven插件运行方式三、生成配置 generatorConfig.xml MyBatis3Simple风格MyBatis3风格MyBatis3DynamicSql风格 四、Java代码运行方式五、MGB生成全部表六、增加Ext包七、Git提交总结 前言 本文我们主要实战Mybatis官方的代码生成器…