threadlocal值存在当前线程的threadlocals字段(threadlocalmap类型),由线程私有持有;entry继承weakreference,避免内存泄漏;set/get触发map初始化;设计核心是线程隔离与弱引用清理。

想真正掌握 ThreadLocal 底层原理,不能只看 API 调用或写几个 demo,得顺着它的数据流向和生命周期去读源码——重点不是“它有哪些方法”,而是“值存在哪、谁在管、什么时候清理、为什么这么设计”。下面这四条路径,是实战中验证过最有效的源码阅读方式。
盯住 Thread 类里的 threadLocals 字段
ThreadLocal 的“本地”不是指它自己存数据,而是把数据塞进了当前线程对象内部。打开 java.lang.Thread 源码,找到这行:
ThreadLocal.ThreadLocalMap threadLocals = null;这就是一切的起点。每个线程启动时 threadLocals 默认为 null,只有第一次调用 set() 或 get() 时才被创建。你要确认三点:
- 这个 map 是线程私有的,不同线程的 threadLocals 完全无关
- map 的类型是 ThreadLocal 的静态内部类 ThreadLocalMap,不是 HashMap
- 它不是 ThreadLocal 的成员变量,ThreadLocal 实例本身不存任何业务数据
精读 ThreadLocalMap 的 Entry 结构
打开 ThreadLocal.ThreadLocalMap,重点关注静态内部类 Entry:
static class Entry extends WeakReference关键就在这两处:
- key 是弱引用(WeakReference 包装的 ThreadLocal 实例),这是防止内存泄漏的核心设计
- value 是强引用,指向你 set 进去的实际对象(比如 SimpleDateFormat、traceId 字符串等)
- Entry 数组用的是开放地址法(不是链表/红黑树),哈希冲突靠 nextIndex 线性探测解决
读到这里,你就明白为什么“ThreadLocal 实例被回收后,Entry 的 key 变成 null,但 value 还占着内存”——这就是泄漏的根源。
跟踪 set/get/remove 的完整链路
不要跳着看,拿一个调用链从头跟到底。例如从 set() 开始:
- Thread.currentThread() → 获取当前线程对象
- getMap(t) → 返回 t.threadLocals(可能为 null)
- createMap(t, value) 或 map.set(this, value) → this 是当前 ThreadLocal 实例,作为 key
再对比 get():
- 同样先 getMap(t),找不到就 setInitialValue()
- map.getEntry(this) → 在 Entry[] 中按 threadLocalHashCode 定位槽位
- 过程中会触发 cleanSomeSlots(),主动扫描并清理 key == null 的 stale entry
你会发现:set 和 get 不只是存取,还附带了“顺手打扫”的逻辑,这是 JDK 做的兜底保护。
结合线程池场景反推问题本质
单纯看单线程源码容易忽略真实风险。把 ThreadLocal 放进 ThreadPoolExecutor 里跑一跑:
- 线程复用导致 threadLocals 不会被销毁,旧 value 长期滞留
- 如果忘记调用 remove(),下次同一线程处理新请求时,get() 可能拿到上一次遗留的值(用户串号、事务污染)
- 观察 ThreadLocalMap.resize() 行为:扩容时会遍历所有 Entry,自动过滤掉 key == null 的条目——但前提是得走到那一步
这时候再回看 remove() 方法,就会理解它不只是“删一个值”,而是主动触发 expungeStaleEntry(),把整个 hash 槽位链路上的脏数据一并清掉。
不复杂但容易忽略:ThreadLocal 的威力不在语法糖,而在它把“线程生命周期”和“变量生命周期”做了显式绑定。读懂 threadLocals 字段、Entry 的弱引用设计、以及 set/get 中隐含的清理动作,才算真正穿过 API 表面,触达了 JVM 级别的线程隔离机制。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











