ThreadLocal 原理,一次讲透 🧵
ThreadLocal 像是给每个线程发了一把「私人抽屉钥匙」🔑——抽屉都在同一栋楼(JVM)里,但钥匙不通用。
一、ThreadLocal 是干嘛的?🤔
一句话总结:
ThreadLocal = 线程私有变量容器。
它让 同一个变量名,在不同线程里各活各的,互不打扰:
- ❌ 不用加锁
- ❌ 不会线程不安全
- ✅ 性能轻快,像春风一样
⚠️ 重要澄清:
ThreadLocal 不是用来共享数据的,
它的使命是——隔离。
常见使用场景:
- 用户登录信息(userId)
- TraceId / 日志上下文 🧾
- 数据库连接、事务上下文
二、怎么用?简单到离谱 🧩
ThreadLocal 只有三个灵魂方法:
set():往线程抽屉里放东西 📦get():从抽屉里拿东西 🫴remove():清空抽屉(很重要!)🧹
1 | public class ThreadLocalDemo { |
输出结果:
1 | Thread-A 获取到的用户是:Thread-A |
✨ 看起来像共享变量,用起来像私有财产。
三、底层原理:秘密藏在 Thread 里 🧠
重点来了。
1️⃣ 每个 Thread 内部都有一个小 Map
1 | ThreadLocal.ThreadLocalMap threadLocals; |
结构关系:
1 | Thread |
🧩 结论:
不是 ThreadLocal 存数据,而是 Thread 存数据。
ThreadLocal 只是:
👉 一把「定位钥匙」🔑
2️⃣ 为什么要用 Map?
因为:
一个线程里,可以有很多个 ThreadLocal。
所以必须:
- 一个 Thread
- 对应一个 Map
- key = ThreadLocal
- value = 真正的数据
就这么直白。
四、内存泄漏:ThreadLocal 最容易翻车的地方 💥
这是 面试必杀技 + 线上事故高发区。
4️⃣-1️⃣ 问题根源:弱引用 + 线程不死
ThreadLocalMap 的 Entry 长这样:
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
- key:
ThreadLocal→ 弱引用 - value:业务对象 → 强引用
📊 Java 四种引用类型速查表(补充理解)
| 引用类型 | 类名 | GC 是否回收 | 常见场景 | 一句话理解 |
|---|---|---|---|---|
| 强引用 | 普通对象 | ❌ 不回收 | 业务对象、集合 | 只要你还指着它,GC 不敢动 |
| 软引用 | SoftReference |
⚠️ 内存紧张回收 | 缓存 | 内存不够时的备用胎 |
| 弱引用 | WeakReference |
✅ 立刻回收 | ThreadLocal key | GC 一来,马上放手 |
| 虚引用 | PhantomReference |
✅ 随时 | 资源回收监控 | 只通知,不干预 |
1 | 存活优先级:强 > 软 > 弱 > 虚 |
📌 放回 ThreadLocal 场景:
1 | Entry |
4️⃣-2️⃣ 翻车过程(高清无码版)
1️⃣ ThreadLocal 没有强引用了
2️⃣ GC 发生:
- ThreadLocal ✅ 被回收
- Entry.key = null
3️⃣ 但是:
- 线程池线程还活着
- ThreadLocalMap 还在
- Entry 还在
- value 还在 😱
🎯 结果:
value 永远挂在线程上 —— 内存泄漏。
4️⃣-3️⃣ 正确姿势:用完就 remove 🧹
1 | try{ |
📌 记住这句话:
ThreadLocal 不 remove,迟早要背锅。
五、Hash 冲突:ThreadLocalMap 怎么解决?⚔️
ThreadLocalMap 本质是一个:
🚫 没有链表的 HashMap
5️⃣-1️⃣ 冲突策略:线性探测
- 当前位置被占?
- 👉 往后挪一格
- 👉 继续挪
像找停车位 🚗。
5️⃣-2️⃣ 为什么一般问题不大?
- ThreadLocal 数量通常不多
- 生命周期短
- Map 容量有限
📌 官方建议:
- ThreadLocal 定义为
static final - 避免频繁创建
六、一句话总结 🎯
- ThreadLocal = 线程私有抽屉 🗄️
- 数据存在 Thread,不在 ThreadLocal
- key 是弱引用,value 是强引用
- 不 remove = 内存泄漏 💣
- 线程池 + ThreadLocal = 高危组合 ☠️
用得好,它是神器;
用不好,它是“内存慢性毒药”。
—— 谨慎,但别害怕 😉
Comments




