尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
详解ThreadLocal
一、是什么简单一句话ThreadLocal 给每个线程单独创建一份变量副本A 线程修改副本不影响 B 线程。ThreadLocal是线程本地变量它可以在同一个线程内共享数据线程之间互相隔离。核心数据不是存在 ThreadLocal 对象里而是存在当前 Thread 对象的threadLocals成员变量中。ThreadLocal 只是一个存取这个 Map 的工具入口。ThreadLocal 对象本身只是一个 “钥匙”不存数据真正存数据的仓库是每个 Thread线程对象自己身上自带的一个 MapthreadLocals。ThreadLocalString tl new ThreadLocal();这个tl只是一把钥匙不是仓库。每个线程各自拥有一个独立仓库threadLocals。调用tl.set(abc)拿钥匙tl往当前线程自己的仓库存入一对keytlvalueabc。拆开看结构## 举个简单例子串起来 ThreadLocalString tl1 new ThreadLocal(); ThreadLocalString tl2 new ThreadLocal(); // 线程A tl1.set(A的数据1); tl2.set(A的数据2); // 线程A的threadLocals里面 // entry1: keytl1, valueA的数据1 // entry2: keytl2, valueA的数据2 String valA1 tl1.get(); // 拿到 A的数据1 String valA2 tl2.get(); // 拿到 A的数据2 // 线程B tl1.set(B的数据1); // 线程B自己独立的threadLocals // entry1: keytl1, valueB的数据1 String valB1 tl1.get(); // 拿到 B的数据1 String valB2 tl2.get(); // null线程B没有往tl2存过值 同一个 tl1 对象在线程 A、B 各自的 Map 里是两条完全独立的 entry互不干扰。重点数据value放在t.threadLocals属于线程对象。ThreadLocal对象只是作为 map 的 key。同一个 ThreadLocal 钥匙在不同线程访问的是不同仓库。✅ 正确理解每个线程自带独立 MapThreadLocal 只是 key。顺着这个原理解释几个面试点1. 为什么线程销毁ThreadLocalMap 就销毁因为threadLocals是 Thread 的成员变量。线程对象被 GC这个 Map 跟着一起回收。⚠️但是只有线程对象被 GC 的时候ThreadLocalMap才会跟着销毁。线程池里的线程不会销毁线程对象一直存活、反复复用所以它里面的ThreadLocalMap对象就一直保存在线程对象身上不会被回收。。2. get () 的时候发生了什么tl.get();获取当前线程Thread.currentThread()获取当前线程身上的threadLocalsmap以thistl对象为 key 去 map 里查 value返回 value全程没有任何数据存在 tl 对象本身。极简比喻Thread 每个人threadLocals 每个人随身带的小本子ThreadLocal 固定标签比如【用户信息】这个标签操作tl.set(张三)→ 在当前这个人的小本子上找到【用户信息】标签写下张三。tl.get()→ 在当前这个人的小本子上读取【用户信息】标签内容。不同的人线程各自有自己本子同一个标签写在不同本子上互不影响。标签ThreadLocal全局共用本子threadLocals每人一份。二、底层结构Thread类内部ThreadLocalMap threadLocals null;ThreadLocalMap是 Thread 的内部属性不是 HashMap是 ThreadLocal 自定义的哈希表。key弱引用指向ThreadLocal对象value我们存入的业务数据强引用Thread └── ThreadLocalMap ├── Entry[] table ├── Entry(WeakReferenceThreadLocal? key, Object value)弱引用的作用Entry的 key 是WeakReferenceThreadLocal?弱引用 当外部 ThreadLocal 引用被置为 nullGC 可以回收 ThreadLocal 对象。如果 key 是强引用即使外部 ThreadLocal 引用消失因为 Entry 还持有强引用ThreadLocal 对象无法 GC造成内存泄漏。⚠️但是value 仍然是强引用这就是内存泄漏根源key 被 GC 清除后Entry 变成[null, value]Thread 如果一直存活如线程池value 强引用不会释放value 内存泄漏。 所以必须手动调用remove()。三、核心方法set(T value)获取当前线程Thread获取线程内的ThreadLocalMapmap.put (this, value)key 是当前 ThreadLocal 实例get()获取当前线程的 ThreadLocalMapthis 作为 key 查询 Entry返回 value没有则初始化返回 nullremove()获取当前线程 map删除当前 ThreadLocal 对应的 Entry清除 key 和 value解决内存泄漏。四、使用示例public class ThreadLocalDemo { // 创建ThreadLocal实例 private static final ThreadLocalString threadLocal new ThreadLocal(); public static void main(String[] args) { new Thread(() - { threadLocal.set(线程A的数据); System.out.println(threadLocal.get()); // 线程A的数据 threadLocal.remove(); // 必须remove }).start(); new Thread(() - { threadLocal.set(线程B的数据); System.out.println(threadLocal.get()); // 线程B的数据 threadLocal.remove(); }).start(); } }两个线程使用同一个threadLocal对象但是各自存的数据互不干扰。五、典型使用场景传递链路上下文比如用户登录信息、traceId同一个线程的多个方法不用反复传参拦截器设置业务代码直接 get。SimpleDateFormat / DateFormat非线程安全用 ThreadLocal 每个线程持有一个实例避免并发异常。事务管理Spring 事务Connection绑定到当前线程保证同一个线程拿到同一个数据库连接。六、两大坑点1. 内存泄漏原因ThreadLocalMap.Entry 的 key 弱引用value 强引用。如果线程长期存活线程池key 被 GC 后value 还被 Entry 强引用无法释放。✅ 解决方案用完必须调用 remove ()即删除当前线程 ThreadLocalMap 里以当前 ThreadLocal 对象为 key 的那个 Entry。Entry 一条记录包含两部分key弱引用指向 ThreadLocal 实例 value强引用指向你存的数据2. 线程池下的数据污染值传递问题线程池会复用线程 线程 A 执行完任务没有 remove。下一个任务复用这个线程直接 get 拿到旧的脏数据。例子 线程池线程 1 执行任务 1set userId100没有 remove。 任务 2 过来复用线程 1直接 get () 得到 userId100不是预期 null造成数据污染。✅ 解决方案任务执行完毕finally块调用remove()。七、ThreadLocalMap 哈希冲突怎么解决HashMap 用链表 红黑树 ThreadLocalMap 使用线性探测 hash 冲突时向后寻找下一个空槽位存放 Entry。好处结构简单缺点冲突多的时候性能下降。扩容阈值table 长度的 2/3。同时会清理 key 为 null 的 Entry帮助释放内存。八、ThreadLocal vs synchronizedThreadLocalsynchronized原理每个线程独立副本不共享变量同一共享变量加锁排队访问侧重点空间换时间线程隔离时间换空间保证共享数据安全场景线程私有数据上下文传递多线程修改共享资源一句话区分synchronized大家抢同一个东西排队用。ThreadLocal每人单独一份东西互不干扰。九、扩展Q1ThreadLocal 能不能被子线程继承不能。子线程是新 ThreadthreadLocals 是全新空 map。如果需要InheritableThreadLocal可以把父线程的 ThreadLocal 值传递给子线程。局限线程池场景失效因为线程复用不会重新复制。Q2ThreadLocal 为什么 key 设计成弱引用如果 key 是强引用ThreadLocal 引用置 null 后ThreadLocalMap.Entry 还持有强引用ThreadLocal 对象永远无法 GCkey 也内存泄漏。弱引用保证 ThreadLocal 对象本身可以被 GC只留下 value 泄漏风险由 remove 解决。Q3ThreadLocal 推荐定义成 static final 吗推荐。staticThreadLocal 实例属于类不需要每个对象新建final防止 ThreadLocal 引用被重新赋值。private static final ThreadLocalUser USER_TL new ThreadLocal();Q4什么时候用 InheritableThreadLocal主线程开启新线程希望子线程自动拿到父线程的 ThreadLocal 数据。线程池场景慎用线程复用继承只会在线程创建时拷贝一次。面试真题题目说一下 ThreadLocal 原理内存泄漏问题线程池场景会出现什么问题如何避免参考答案ThreadLocal 实现线程本地变量数据存储在当前 Thread 对象内部的 ThreadLocalMap 中key 是 ThreadLocal 实例Entry 的 key 使用弱引用。内存泄漏Entry 的 value 是强引用如果线程长期存活key 被 GC 后 value 无法释放。线程池场景风险线程复用如果任务执行完没有 remove下一个任务拿到旧数据造成数据污染。 解决用完在 finally 中调用 remove ()。面试题ThreadLocal 代码改错题目下面这段代码使用线程池 ThreadLocal请问存在什么问题会出现什么现象如何修复public class ThreadLocalBugDemo { private static final ThreadLocalInteger userIdTl new ThreadLocal(); private static final ExecutorService pool Executors.newFixedThreadPool(2); public static void main(String[] args) { for (int i 1; i 5; i) { int userId i; pool.submit(() - { userIdTl.set(userId); System.out.println(当前线程: Thread.currentThread().getName() , userId: userIdTl.get()); // 忘记 remove }); } } }参考答案问题线程池复用线程ThreadLocal 脏数据污染线程池只有 2 个线程会反复复用。任务 1 在线程 1 设置 userId1任务执行完没有 remove。任务 3 分配到线程 1执行userIdTl.set(3)会覆盖看起来没问题但是如果某个任务分支逻辑没有执行 set直接get()就会读到上一个任务遗留的旧 userId。举个极端场景pool.submit(() - { // 某些业务判断没有执行 set System.out.println(userIdTl.get()); // 拿到上一个任务残留的旧userId });现象部分任务读到不属于自己的 userId业务逻辑错乱很难复现的偶现 bug。修复方案任务执行完毕finally 块调用 remove ()保证无论正常 / 异常退出都清理 ThreadLocalpool.submit(() - { try { userIdTl.set(userId); System.out.println(当前线程: Thread.currentThread().getName() , userId: userIdTl.get()); } finally { userIdTl.remove(); // 清理防止脏数据 内存泄漏 } });为什么会有内存泄漏为什么 key 设计为弱引用还会泄漏回答ThreadLocalMap.Entry 的 key 是弱引用value 是强引用。当 ThreadLocal 引用置为 nullkey 会被 GCEntry 变成[null, value]。线程池中的线程长期存活Thread 对象不会被回收Entry 还在 Map 里value 强引用无法释放造成 value 内存泄漏。remove()会把整个 Entry 删除key 和 value 都释放同时解决脏数据和内存泄漏。为什么 ThreadLocal 不能用在分布式场景传递用户信息ThreadLocal 是线程内本地变量只在当前 JVM 当前线程有效。一旦发生 RPC 调用请求会到另一个服务新开线程ThreadLocal 数据丢失。分布式链路用header 传递token/traceId。InheritableThreadLocal 面试题题目InheritableThreadLocal 的作用是什么有什么局限性线程池场景下还能用吗参考答案作用父线程创建子线程的时候会把父线程InheritableThreadLocal的值拷贝一份给新建出来的子线程。private static final InheritableThreadLocalString tl new InheritableThreadLocal(); public static void main(String[] args) { tl.set(父线程数据); new Thread(()-{ System.out.println(tl.get()); // 输出父线程数据 }).start(); }局限性只有子线程新建的时候才拷贝一次后续父线程修改子线程不会同步更新。线程池场景失效线程池线程是提前创建好的不是每次任务新建线程。父线程提交任务时不会触发拷贝子线程拿不到父线程的值。拓展如果在线程池想要透传上下文阿里开源的TransmittableThreadLocal(TTL)专门解决线程池场景上下文传递。问ThreadLocalMap 扩容和清理过期 Entry 的机制答ThreadLocalMap 初始容量 16负载阈值是容量的 2/3。每次 set 的时候会扫描 table清理 key 为 null 的 EntryexpungeStaleEntry清理过程会连带把 value 置空。当元素数量达到阈值执行 rehash再次全量清理过期 entry如果清理后仍然超过阈值才执行扩容容量翻倍。
RELATED

相关推荐

基于 Gatsby 与 Netlify 的个人网站第四次迭代:v4 项目安装、构建与主题体系全解析

基于 Gatsby 与 Netlify 的个人网站第四次迭代:v4 项目安装、构建与主题体系全解析

前端 【免费下载链接】v4 Fourth iteration of my personal website built with Gatsby 项目地址: https://gitcode.com/gh_mirrors/v41/v4 点击查看 免费下载 本指南以当前仓库根目录的 README.md 为主体,围绕 brittanychiang.com 个人网站的第四次迭代…

📅 2026/10/8 19:29:35
LoRa自组网三大技术路线:洪泛、路由与网络栈的工程权衡

LoRa自组网三大技术路线:洪泛、路由与网络栈的工程权衡

1. 为什么LoRa自组网必须在“洪泛、路由、网络栈”三者间做取舍?我第一次把LoRa节点撒进山林做土壤温湿度监测时,用的是最朴素的洪泛方案:每个节点收到数据就原样广播出去,靠信号强度和重传次数硬扛丢包。结果第三天,整…

📅 2026/10/8 19:29:35
gsd-2 技能库实战:React 最佳实践中“延迟 await“(Defer Await Until Needed)消除非必要异步阻塞

gsd-2 技能库实战:React 最佳实践中“延迟 await“(Defer Await Until Needed)消除非必要异步阻塞

人工智能AI Agent代码智能体Agent 编排CLIAI 应用 【免费下载链接】gsd-2 A powerful meta-prompting, context engineering and spec-driven development system that enables agents to work for long periods of time autonomously without losing track of the big picture…

📅 2026/10/8 19:24:34
MORE NEWS

更多资讯

📰

工业电源路径保护:eFuse与TVS阵列协同设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

银河麒麟离线安装QGis:依赖闭环与批量部署实战

简介:本资源面向在银河麒麟操作系统上需要离线部署QGIS的用户,尤其是使用国产CPU架构、内网环境或无法联网的科研与生产场景。QGIS作为开源地理信息系统,常用于地理数据采集、管理、分析与展示,而银河麒麟基于Linux内核&#xff0…

📰

Kettle实战:学生成绩导入清洗与排名自动化全流程解析

搞数据的人,估计都逃不过这么一关:教务老师发来一堆学生成绩表,Excel一个班一个格式,缺考的空着、学号带着空格、数字存成文本;领导那边要的排名还特别讲究“同分同名次,下一个名次跳过”。我之前接到这类“…

📰

Grok Bot:轻量级数字员工的落地实践与架构设计

1. 这不是“AI助手”,而是一类新型数字员工的实践起点最近在多个技术社群和内部协作平台里,频繁看到“Grok Bot 可当员工雇佣”这个说法。它不是一句营销口号,也不是某家公司的宣传通稿,而是真实发生在一线团队中的工作流重构现象…

📰

Hoppscotch自部署实战:Docker Compose与源码安装详解

Hoppscotch 这个项目最早吸引我,不是因为它挂着“开源版 Postman”的名头,而是因为它把 API 调试这件事直接塞进了浏览器标签页。F12 打开的一瞬间,接口调试工具就已经在那里了,不用再启动一个重型客户端。作为一个每天要和十几台…

📰

华为云AgentArts实战:信贷预审智能体从搭建到调优全链路

金融信贷这个行业,过去几年我最大的感受就是:风控和获客这两件事,正在从"人盯人"变成"模型盯人",再变成"智能体盯流程"。华为云智果AgentArts这个平台,说白了就是让你把大模型能力、业务…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬