快船:网球战术免安装绿色中文版
134M · 2025-11-16
ThreadLocal是用来实现线程隔离的数据存储。 它能让每个线程独立持有一份变量副本,互不干扰。
ThreadLocal = 每个线程都有自己的“私有小仓库”。
示例:
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();
输出:
A线程的数据
B线程的数据
两条线程各用各的,不会串。
ThreadLocal提供了一种线程内独享的变量机制,使每个线程都能有自己独立的变量副本。每个线程内部维护一个ThreadLocalMap!!!这个ThreadLocalMap用于存储线程独立的变量副本。ThreadLocalMap以ThreadLocal实例作为key,以线程独立的变量副本作为值。不同的线程通过ThreadLocal获取各自的变量副本,而不会影响其他线程的数据。
误区:
不是将ThreadLocal看作一个map,然后每个线程都是key,这样每个线程去调用ThreadLocal.get的时候,将自身作为key去map中找,获取各自的value。这个是错误的!!!这样ThreadLocal就变成了共享变量了。多个线程竞争ThreadLocal,又得加锁,又回到原点。
需要在每个线程的本地都存一份值,每个线程需要有个变量,来存储这些需要本地化资源的值,并且值有可能有多个,这就需要用到map。就是刚才讲的。
比如:现在有三个ThreadLocal对象,两个线程
ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
那么他们对应的关系就是:
这样一来就满足了本地化资源的需求,每个线程维护自己的变量,互不干扰,实现了变量的线程隔离,同时也满足存储多个本地变量的需求。
Thread对象里边会有一个ThreadLocalMap,用来保存本地变量。
源码:
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的。
内存泄漏是指:程序中不再使用的对象仍然被引用着,导致垃圾回收器(GC)无法释放它们的内存空间。
示例:静态集合导致的内存泄漏
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() 结束,它也不会被销毁。byte[] 数组,然后存进这个集合。ArrayList 里的引用)。memoryHolder 引用着。java.lang.OutOfMemoryError: Java heap space
只要释放引用,GC 就能正常工作: memoryHolder.clear(); // 清空集合
又比如再ThreadLocal发生的内存溢出问题
ThreadLocal<byte[]> local = new ThreadLocal<>();
local.set(new byte[1024 * 1024]); // 1MB 数据
local = null; // ThreadLocal对象没了
// 但 ThreadLocalMap 还在引用 value → 内存泄漏
调用threadLocal.remove();就不会发生这个问题。
ThreadLocal为什么会发生内存泄漏呢?刚才已经讲明了
key 是弱引用(WeakReference)
value 是强引用(Object)
当我们执行
ThreadLocal<User> local = new ThreadLocal<>();
local.set(new User("xxx"));
底层会变成:
| 存放位置 | 内容 |
|---|---|
| 当前线程的 ThreadLocalMap | key = ThreadLocal(弱引用) |
| value = User(强引用) |
如果稍后你把: local = null;
那此时:
null)这就导致:
ThreadLocalMap 里有个 “key=null、value=仍然存在” 的 Entry,value 永远不会被释放
那你可能会说,把里边的value也变成弱引用不就好了。
× 不行, 如果 value 是弱引用,那么只要发生 GC,value 可能被清理掉。然后你下一次 get() 时,就拿不到数据了,线程内部状态会丢失。
举个例子 :
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 呢
private static final ThreadLocal<User> USER_HOLDER = new ThreadLocal<>();
优点:
但问题是:
| 问题 | 原因 |
|---|---|
| value 仍然留在线程中 | 如果线程池复用线程,而你没调用 remove(),老的 value 仍然在 |
| static 生命周期过长 | 整个应用期间一直存在,可能造成全局数据滞留 |
| 大对象浪费内存 | 即使不访问,也常驻内存(尤其在线程池中) |
所以它确实能缓解泄漏问题,但不能彻底解决。
线程池线程不会销毁!
每个线程都有自己的 ThreadLocalMap。
如果你使用了 ThreadLocal,但没有及时 remove():
所以线程池 + ThreadLocal 是高危组合。
ThreadLocalMap 的设计者(Doug Lea)意识到这个风险,
所以在源码中加入了“懒惰清理” 逻辑:
每次调用 get()、set()、remove() 时,会顺带清理掉 key=null 的 Entry。
private void expungeStaleEntry(int i) {
table[i].value = null;
table[i] = null;
}
但注意:
如果你的代码永远不再访问这个 ThreadLocal(比如请求结束后),那清理永远不会触发!
所以这不是自动回收机制,只是懒清理。
正确用法
关键点就是 —— 用完要 remove()。
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 又长期存在。