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 又长期存在。