一、是什么
简单一句话:
ThreadLocal 给每个线程单独创建一份变量副本;
A 线程修改副本,不影响 B 线程。
ThreadLocal是线程本地变量,它可以在同一个线程内共享数据,线程之间互相隔离。
核心:
数据不是存在 ThreadLocal 对象里,而是存在当前 Thread 对象的
threadLocals成员变量中。ThreadLocal 只是一个存取这个 Map 的工具入口。
ThreadLocal 对象本身只是一个 “钥匙”,不存数据;
真正存数据的仓库,是每个 Thread(线程)对象自己身上自带的一个 Map(threadLocals)。
ThreadLocal<String> tl = new ThreadLocal<>();这个
tl只是一把钥匙,不是仓库。每个线程,各自拥有一个独立仓库
threadLocals。调用
tl.set("abc"):拿钥匙tl,往当前线程自己的仓库存入一对key=tl,value="abc"。拆开看结构
## 举个简单例子串起来 ``` ThreadLocal<String> tl1 = new ThreadLocal<>(); ThreadLocal<String> tl2 = new ThreadLocal<>(); // 线程A tl1.set("A的数据1"); tl2.set("A的数据2"); // 线程A的threadLocals里面: // entry1: key=tl1, value="A的数据1" // entry2: key=tl2, value="A的数据2" String valA1 = tl1.get(); // 拿到 "A的数据1" String valA2 = tl2.get(); // 拿到 "A的数据2" // 线程B tl1.set("B的数据1"); // 线程B自己独立的threadLocals: // entry1: key=tl1, value="B的数据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 钥匙,在不同线程,访问的是不同仓库。
✅ 正确理解:每个线程自带独立 Map,ThreadLocal 只是 key。
顺着这个原理,解释几个面试点
1. 为什么线程销毁,ThreadLocalMap 就销毁?
因为
threadLocals是 Thread 的成员变量。线程对象被 GC,这个 Map 跟着一起回收。⚠️
但是!!!
只有线程对象被 GC 的时候,
ThreadLocalMap才会跟着销毁。线程池里的线程不会销毁,线程对象一直存活、反复复用,所以它里面的
ThreadLocalMap对象就一直保存在线程对象身上,不会被回收。。2. get () 的时候发生了什么
tl.get();
- 获取当前线程
Thread.currentThread()- 获取当前线程身上的
threadLocalsmap- 以
this(tl对象)为 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(WeakReference<ThreadLocal<?>> key, Object value)弱引用的作用
Entry的 key 是WeakReference<ThreadLocal<?>>(弱引用): 当外部 ThreadLocal 引用被置为 null,GC 可以回收 ThreadLocal 对象。
如果 key 是强引用:即使外部 ThreadLocal 引用消失,因为 Entry 还持有强引用,ThreadLocal 对象无法 GC,造成内存泄漏。
⚠️但是!value 仍然是强引用!这就是内存泄漏根源!
- key 被 GC 清除后,Entry 变成
[null, value] - Thread 如果一直存活(如线程池),value 强引用不会释放,value 内存泄漏。
👉 所以必须手动调用remove()。
三、核心方法
set(T value)- 获取当前线程
Thread - 获取线程内的
ThreadLocalMap - map.put (this, value),key 是当前 ThreadLocal 实例
- 获取当前线程
get()- 获取当前线程的 ThreadLocalMap
- this 作为 key 查询 Entry,返回 value;没有则初始化,返回 null
remove()- 获取当前线程 map,删除当前 ThreadLocal 对应的 Entry,清除 key 和 value,解决内存泄漏。
四、使用示例
public class ThreadLocalDemo { // 创建ThreadLocal实例 private static final ThreadLocal<String> 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 执行任务 1,set userId=100,没有 remove。 任务 2 过来复用线程 1,直接 get () 得到 userId=100,不是预期 null,造成数据污染。
✅ 解决方案:任务执行完毕,finally块调用remove()。
七、ThreadLocalMap 哈希冲突怎么解决?
HashMap 用链表 + 红黑树; ThreadLocalMap 使用线性探测: hash 冲突时,向后寻找下一个空槽位存放 Entry。
好处:结构简单;缺点:冲突多的时候性能下降。
扩容阈值:table 长度的 2/3。同时会清理 key 为 null 的 Entry,帮助释放内存。
八、ThreadLocal vs synchronized
| ThreadLocal | synchronized | |
|---|---|---|
| 原理 | 每个线程独立副本,不共享变量 | 同一共享变量,加锁排队访问 |
| 侧重点 | 空间换时间,线程隔离 | 时间换空间,保证共享数据安全 |
| 场景 | 线程私有数据,上下文传递 | 多线程修改共享资源 |
一句话区分:
synchronized:大家抢同一个东西,排队用。
ThreadLocal:每人单独一份东西,互不干扰。
九、扩展
Q1:ThreadLocal 能不能被子线程继承?
不能。
子线程是新 Thread,threadLocals 是全新空 map。
如果需要:InheritableThreadLocal,可以把父线程的 ThreadLocal 值传递给子线程。
局限:线程池场景失效,因为线程复用,不会重新复制。
Q2:ThreadLocal 为什么 key 设计成弱引用?
如果 key 是强引用:ThreadLocal 引用置 null 后,ThreadLocalMap.Entry 还持有强引用,ThreadLocal 对象永远无法 GC,key 也内存泄漏。
弱引用保证 ThreadLocal 对象本身可以被 GC,只留下 value 泄漏风险,由 remove 解决。
Q3:ThreadLocal 推荐定义成 static final 吗?
推荐。
- static:ThreadLocal 实例属于类,不需要每个对象新建;
- final:防止 ThreadLocal 引用被重新赋值。
private static final ThreadLocal<User> 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 ThreadLocal<Integer> 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 设置 userId=1,任务执行完没有 remove。
任务 3 分配到线程 1,执行userIdTl.set(3)会覆盖,看起来没问题;
但是!如果某个任务分支逻辑没有执行 set,直接get(),就会读到上一个任务遗留的旧 userId。
举个极端场景:
pool.submit(() -> { // 某些业务判断,没有执行 set System.out.println(userIdTl.get()); // 拿到上一个任务残留的旧userId!! });
现象
部分任务读到不属于自己的 userId,业务逻辑错乱,很难复现的偶现 bug。
修复方案
任务执行完毕,finally 块调用 remove (),保证无论正常 / 异常退出,都清理 ThreadLocal:
pool.submit(() -> { try { userIdTl.set(userId); System.out.println("当前线程:" + Thread.currentThread().getName() + ", userId:" + userIdTl.get()); } finally { userIdTl.remove(); // 清理,防止脏数据 + 内存泄漏 } });为什么会有内存泄漏?为什么 key 设计为弱引用还会泄漏?
回答:
ThreadLocalMap.Entry 的 key 是弱引用,value 是强引用。
当 ThreadLocal 引用置为 null,key 会被 GC,Entry 变成[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 InheritableThreadLocal<String> 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 的 Entry(expungeStaleEntry);
清理过程会连带把 value 置空。
当元素数量达到阈值,执行 rehash,再次全量清理过期 entry,如果清理后仍然超过阈值,才执行扩容,容量翻倍。