1.ThreadLoacal是干什么的?
ThreadLocal是用来实现线程隔离的数据存储。 它能让每个线程独立持有一份变量副本,互不干扰。
ThreadLocal = 每个线程都有自己的"私有小仓库"。
示例:
csharp
ThreadLocal<String> local = new ThreadLocal<>();
new Thread(() -> {
local.set("A线程的数据");
System.out.println(local.get());
}).start();
new Thread(() -> {
local.set("B线程的数据");
System.out.println(local.get());
}).start();
输出:
css
A线程的数据
B线程的数据
两条线程各用各的,不会串。
2.ThreadLocal的底层原理
ThreadLocal提供了一种线程内独享的变量机制,使每个线程都能有自己独立的变量副本。每个线程内部维护一个ThreadLocalMap!!!这个ThreadLocalMap用于存储线程独立的变量副本。ThreadLocalMap以ThreadLocal实例作为key,以线程独立的变量副本作为值。不同的线程通过ThreadLocal获取各自的变量副本,而不会影响其他线程的数据。
设计原理
误区:
不是将ThreadLocal看作一个map,然后每个线程都是key,这样每个线程去调用ThreadLocal.get的时候,将自身作为key去map中找,获取各自的value。这个是错误的!!!这样ThreadLocal就变成了共享变量 了。多个线程竞争ThreadLocal,又得加锁,又回到原点。
需要在每个线程的本地都存一份值,每个线程需要有个变量,来存储这些需要本地化资源的值,并且值有可能有多个,这就需要用到map。就是刚才讲的。
比如:现在有三个ThreadLocal对象,两个线程
ini
ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
那么他们对应的关系就是:

这样一来就满足了本地化资源的需求,每个线程维护自己的变量,互不干扰,实现了变量的线程隔离,同时也满足存储多个本地变量的需求。
底层源码
Thread对象里边会有一个ThreadLocalMap,用来保存本地变量。
源码:
java
public class Thread {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
但是这个map是ThreadLocal的静态内部类,这个变量名称是threadLocals。
ThreadLocalMap 是 ThreadLocal 的静态内部类,是为了表达逻辑归属关系,但又不让 Map 对 ThreadLocal 实例形成强引用,从而避免内存泄漏。
ThreadLocalMap的定义:

里边有一个Entity数组,继承了WeakReference即弱引用 。但是这个不是说是数组弱引用,而是Entry里边的super(k),这个key才是弱引用。
所以ThreadLocalMap里边有个数组,数组的ket就是ThreadLocal对象, value就是我们要保存的值。
ThreadLocal.get()方法
从这可以看出为什么不同的线程对同一个ThreadLocal对象调用get方法能得到不同的值。

map.getEntry(this)
key是如何从ThreadLocalMap中找到Entry的。

3.内存泄漏
什么是内存泄漏?
内存泄漏是指:程序中不再使用的对象仍然被引用着,导致垃圾回收器(GC)无法释放它们的内存空间。
示例:静态集合导致的内存泄漏
csharp
public class MemoryLeakDemo {
// 静态集合(全局存在,不会被 GC 回收)
private static List<byte[]> memoryHolder = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
// 每次添加 1MB 数据
byte[] data = new byte[1024 * 1024];
memoryHolder.add(data);
System.out.println("第 " + (i + 1) + " 次添加, 当前集合大小: " + memoryHolder.size());
Thread.sleep(100); // 稍微等一下,方便观察
}
}
}
memoryHolder是 静态变量 ,属于类MemoryLeakDemo,会一直存在于 JVM 方法区中。
即使main()结束,它也不会被销毁。- 每次循环创建 1MB 的
byte[]数组,然后存进这个集合。
这些数组都被强引用着 (ArrayList里的引用)。 - 垃圾回收器无法清理这些数组 ,因为它们仍被
memoryHolder引用着。 - 运行一会后你会看到:OOM
makefile
java.lang.OutOfMemoryError: Java heap space
只要释放引用,GC 就能正常工作: memoryHolder.clear(); // 清空集合
又比如再ThreadLocal发生的内存溢出问题
csharp
ThreadLocal<byte[]> local = new ThreadLocal<>();
local.set(new byte[1024 * 1024]); // 1MB 数据
local = null; // ThreadLocal对象没了
// 但 ThreadLocalMap 还在引用 value → 内存泄漏
调用threadLocal.remove();就不会发生这个问题。
ThreadLocal为什么会发生内存泄漏呢?
刚才已经讲明了
key 是弱引用(WeakReference)
value 是强引用(Object)

当我们执行
sql
ThreadLocal<User> local = new ThreadLocal<>();
local.set(new User("xxx"));
底层会变成:
| 存放位置 | 内容 |
|---|---|
| 当前线程的 ThreadLocalMap | key = ThreadLocal(弱引用) |
| value = User(强引用) |
如果稍后你把: local = null;
那此时:
- 这个 ThreadLocal 对象在外部没有强引用
- 因为它是弱引用 → 下一次 GC 会回收它(key 变成
null) - 但 value(User 对象)仍然被强引用着(Map 内部)!
这就导致:
ThreadLocalMap 里有个 "key=null、value=仍然存在" 的 Entry,value 永远不会被释放
那你可能会说,把里边的value也变成弱引用不就好了。
× 不行, 如果 value 是弱引用,那么只要发生 GC,value 可能被清理掉。然后你下一次 get() 时,就拿不到数据了,线程内部状态会丢失。
举个例子 :
sql
ThreadLocal<User> local = new ThreadLocal<>();
local.set(new User("xxx"));
// 这时 value = new User("xxx")
// 如果 value 是 WeakReference,GC 一跑,它就被清了
System.gc();
System.out.println(local.get()); // null
这就破坏了 ThreadLocal 的核心语义:"每个线程拥有独立的一份数据,直到手动清除或线程结束前都能访问"。
那 static ThreadLocal 呢
swift
private static final ThreadLocal<User> USER_HOLDER = new ThreadLocal<>();
优点:
- key 不会被 GC,避免 "key=null" 的 Entry;
- 每次请求都可以覆盖旧值,不容易泄漏。
但问题是:
| 问题 | 原因 |
|---|---|
| value 仍然留在线程中 | 如果线程池复用线程,而你没调用 remove(),老的 value 仍然在 |
| static 生命周期过长 | 整个应用期间一直存在,可能造成全局数据滞留 |
| 大对象浪费内存 | 即使不访问,也常驻内存(尤其在线程池中) |
所以它确实能缓解 泄漏问题,但不能彻底解决。
为什么线程池中更危险?
线程池线程不会销毁!
每个线程都有自己的 ThreadLocalMap。
如果你使用了 ThreadLocal,但没有及时 remove():
- key 被回收(key=null)
- value 还在
- 线程长期存在(被线程池复用)
- 这些"脏 value" 永远留在线程中 → 内存持续上涨 → OOM
所以线程池 + ThreadLocal 是高危组合。
ThreadLocalMap 的"清理机制"
ThreadLocalMap 的设计者(Doug Lea)意识到这个风险,
所以在源码中加入了"懒惰清理" 逻辑:
每次调用 get()、set()、remove() 时,会顺带清理掉 key=null 的 Entry。
csharp
private void expungeStaleEntry(int i) {
table[i].value = null;
table[i] = null;
}
但注意:
如果你的代码永远不再访问这个 ThreadLocal(比如请求结束后),那清理永远不会触发!
所以这不是自动回收机制,只是懒清理。
正确用法
关键点就是 ------ 用完要 remove()。
csharp
private static final ThreadLocal<User> THREAD_LOCAL = new ThreadLocal<>();
public void handleRequest() {
try {
THREAD_LOCAL.set(new User("xxx"));
// ...业务逻辑
} finally {
THREAD_LOCAL.remove(); // ✅ 防止内存泄漏
}
}
推荐写在 finally 块中,无论发生什么都能执行。
总结
ThreadLocal 泄漏的本质不是 ThreadLocal 自身的问题,而是 ThreadLocalMap 的 value 没有被释放,Thread 又长期存在。