ThreadLocal 像是给每个线程发了一把「私人抽屉钥匙」🔑——抽屉都在同一栋楼(JVM)里,但钥匙不通用。


一、ThreadLocal 是干嘛的?🤔

一句话总结:

ThreadLocal = 线程私有变量容器

它让 同一个变量名,在不同线程里各活各的,互不打扰:

  • ❌ 不用加锁
  • ❌ 不会线程不安全
  • ✅ 性能轻快,像春风一样

⚠️ 重要澄清:

ThreadLocal 不是用来共享数据的,
它的使命是——隔离

常见使用场景:

  • 用户登录信息(userId)
  • TraceId / 日志上下文 🧾
  • 数据库连接、事务上下文

二、怎么用?简单到离谱 🧩

ThreadLocal 只有三个灵魂方法:

  • set():往线程抽屉里放东西 📦
  • get():从抽屉里拿东西 🫴
  • remove():清空抽屉(很重要!)🧹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ThreadLocalDemo {

private static final ThreadLocal<String> USER = new ThreadLocal<>();

public static void main(String[] args) throws Exception {

Runnable task = () -> {
try {
// 模拟不同用户请求
String userName = Thread.currentThread().getName();
USER.set(userName);

// 模拟业务处理
Thread.sleep(1000);

System.out.println(Thread.currentThread().getName()
+ " 获取到的用户是:" + USER.get());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// ★★★ 关键:必须清理
USER.remove();
}
};

Thread t1 = new Thread(task, "Thread-A");
Thread t2 = new Thread(task, "Thread-B");

t1.start();
t2.start();

t1.join();
t2.join();
}
}

输出结果:

1
2
Thread-A 获取到的用户是:Thread-A
Thread-B 获取到的用户是:Thread-B

✨ 看起来像共享变量,用起来像私有财产。


三、底层原理:秘密藏在 Thread 里 🧠

重点来了。

1️⃣ 每个 Thread 内部都有一个小 Map

1
ThreadLocal.ThreadLocalMap threadLocals;

结构关系:

1
2
3
4
Thread
└── ThreadLocalMap
├── ThreadLocalA -> valueA
└── ThreadLocalB -> valueB

🧩 结论:

不是 ThreadLocal 存数据,而是 Thread 存数据

ThreadLocal 只是:

👉 一把「定位钥匙」🔑


2️⃣ 为什么要用 Map?

因为:

一个线程里,可以有很多个 ThreadLocal。

所以必须:

  • 一个 Thread
  • 对应一个 Map
  • key = ThreadLocal
  • value = 真正的数据

就这么直白。


四、内存泄漏:ThreadLocal 最容易翻车的地方 💥

这是 面试必杀技 + 线上事故高发区

4️⃣-1️⃣ 问题根源:弱引用 + 线程不死

ThreadLocalMap 的 Entry 长这样:

1
2
3
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
}
  • key:ThreadLocal弱引用
  • value:业务对象 → 强引用

📊 Java 四种引用类型速查表(补充理解)

引用类型 类名 GC 是否回收 常见场景 一句话理解
强引用 普通对象 ❌ 不回收 业务对象、集合 只要你还指着它,GC 不敢动
软引用 SoftReference ⚠️ 内存紧张回收 缓存 内存不够时的备用胎
弱引用 WeakReference ✅ 立刻回收 ThreadLocal key GC 一来,马上放手
虚引用 PhantomReference ✅ 随时 资源回收监控 只通知,不干预
1
存活优先级:强 > 软 > 弱 > 虚

📌 放回 ThreadLocal 场景:

1
2
3
Entry
├── key -> WeakReference<ThreadLocal>
└── value -> 强引用 Object

4️⃣-2️⃣ 翻车过程(高清无码版)

1️⃣ ThreadLocal 没有强引用了

2️⃣ GC 发生:

  • ThreadLocal ✅ 被回收
  • Entry.key = null

3️⃣ 但是:

  • 线程池线程还活着
  • ThreadLocalMap 还在
  • Entry 还在
  • value 还在 😱

🎯 结果:

value 永远挂在线程上 —— 内存泄漏


4️⃣-3️⃣ 正确姿势:用完就 remove 🧹

1
2
3
4
5
6
try{
threadLocal.set(obj);
// 业务逻辑
}finally{
threadLocal.remove();
}

📌 记住这句话:

ThreadLocal 不 remove,迟早要背锅。


五、Hash 冲突:ThreadLocalMap 怎么解决?⚔️

ThreadLocalMap 本质是一个:

🚫 没有链表的 HashMap

5️⃣-1️⃣ 冲突策略:线性探测

  • 当前位置被占?
  • 👉 往后挪一格
  • 👉 继续挪

像找停车位 🚗。

5️⃣-2️⃣ 为什么一般问题不大?

  • ThreadLocal 数量通常不多
  • 生命周期短
  • Map 容量有限

📌 官方建议:

  • ThreadLocal 定义为 static final
  • 避免频繁创建

六、一句话总结 🎯

  • ThreadLocal = 线程私有抽屉 🗄️
  • 数据存在 Thread,不在 ThreadLocal
  • key 是弱引用,value 是强引用
  • 不 remove = 内存泄漏 💣
  • 线程池 + ThreadLocal = 高危组合 ☠️

用得好,它是神器;
用不好,它是“内存慢性毒药”。

—— 谨慎,但别害怕 😉