ThreadLocal 为什么会内存泄漏?
面试频率:★★★★★
工作频率:★★★★★
⚡30 秒速记(复习只看这里)
🟢 一句话
ThreadLocal 是线程本地变量,为每个线程保存独立的数据副本,避免多个线程共享数据导致线程安全问题。
🟡 底层原理
ThreadLocal 不负责保存数据。
数据实际存储:
Thread
↓
ThreadLocalMap
↓
Entry
↓
value
每个线程都有自己的 ThreadLocalMap。
🟠 为什么会内存泄漏?
ThreadLocalMap:
key = ThreadLocal(弱引用)
value = 用户数据(强引用)
当 ThreadLocal 没有强引用:
ThreadLocal对象被GC
↓
key变成null
↓
value还存在
↓
无法释放
导致内存泄漏。
🔴 解决方式
使用:
threadLocal.remove();
清理线程中的数据。
📚 完整知识
一、ThreadLocal 是什么?
ThreadLocal:
为每个线程提供独立变量副本。
例如:
线程A:
ThreadA
↓
userId = 100
线程B:
ThreadB
↓
userId = 200
两个线程数据互不影响。
二、为什么不用普通变量?
例如:
public class UserContext {
private static Long userId;
}
多个线程:
线程A:
userId = 100
线程B:
userId = 200
线程B修改后:
线程A读取:
200
产生数据覆盖。
三、ThreadLocal 使用方式
public class UserContext {
private static ThreadLocal<Long> userId =
new ThreadLocal<>();
public static void setUserId(Long id){
userId.set(id);
}
public static Long getUserId(){
return userId.get();
}
public static void remove(){
userId.remove();
}
}
流程:
请求进入
↓
threadLocal.set()
↓
业务代码任意位置 get()
↓
请求结束 remove()
四、ThreadLocal 底层结构
重点:
ThreadLocal 数据存储在线程里面,而不是 ThreadLocal 对象里面。
结构:
Thread
↓
ThreadLocalMap
↓
Entry
↓
value
例如:
线程A:
ThreadA
ThreadLocalMap
|
Entry
key = ThreadLocal
value = 用户A信息
线程B:
ThreadB
ThreadLocalMap
|
Entry
key = ThreadLocal
value = 用户B信息
虽然:
ThreadLocal 是同一个。
但是:
每个线程数据不同。
五、ThreadLocalMap 为什么不是 HashMap?
ThreadLocalMap 是 ThreadLocal 自己实现的一种 Map。
特点:
key 使用弱引用。
六、什么是弱引用?
普通引用:
ThreadLocal tl = new ThreadLocal<>();
强引用:
变量
↓
ThreadLocal对象
只要变量存在:
对象不会被 GC。
弱引用:
WeakReference
↓
对象
如果没有其他强引用:
GC 可以回收对象。
七、为什么会内存泄漏?
正常:
Thread
↓
ThreadLocalMap
↓
Entry
↓
key = ThreadLocal
value = 用户数据
如果 ThreadLocal 没有引用:
例如:
threadLocal = null;
GC:
ThreadLocal对象被回收
变成:
Thread
↓
ThreadLocalMap
↓
Entry
↓
key = null
value = 用户数据
问题:
Entry 仍然强引用 value。
所以:
value无法释放
产生内存泄漏。
八、为什么线程池环境更容易出现?
普通线程:
执行任务
↓
线程结束
↓
资源释放
问题较小。
线程池:
创建线程
↓
长期存在
↓
不断执行任务
例如 Tomcat:
线程1
处理请求A
↓
处理请求B
↓
处理请求C
如果不 remove:
请求A的数据可能残留在线程中。
九、为什么必须 remove?
正确:
try {
threadLocal.set(userId);
doBusiness();
} finally {
threadLocal.remove();
}
保证:
请求结束
↓
清理线程变量
避免:
- 内存泄漏
- 用户数据串线
十、Spring 中 ThreadLocal 的应用
1. 用户登录信息
例如:
登录用户ID
↓
ThreadLocal
↓
Service
↓
Dao
不用每层传递:
service(userId)
2. Spring 事务
Spring 事务会绑定数据库连接。
类似:
当前线程
↓
ThreadLocal
↓
Connection
保证:
同一个事务中使用同一个数据库连接。
3. 链路追踪
例如:
traceId
↓
ThreadLocal
↓
日志
方便查询一次请求经过哪些服务。
💼 工作场景
错误:
threadLocal.set(userId);
但是:
没有:
threadLocal.remove();
在线程池环境:
可能:
请求A用户信息
↓
残留在线程
↓
请求B读取旧数据
产生严重问题。
🎤 面试回答(30秒)
ThreadLocal 是 Java 提供的线程本地变量,它为每个线程维护独立的数据副本,避免多个线程共享变量导致线程安全问题。ThreadLocal 底层数据实际存储在线程的 ThreadLocalMap 中,key 是 ThreadLocal 的弱引用,value 是强引用。如果 ThreadLocal 对象被回收,但是线程还存活,可能导致 key 为 null 而 value 无法释放,从而产生内存泄漏。因此在线程池环境中使用 ThreadLocal 后,需要及时调用 remove 方法清理。
🎯 面试追问
Q1:ThreadLocal 数据存在哪里?
答:
存在线程对象的 ThreadLocalMap 中。
不是 ThreadLocal 自己保存。
Q2:为什么 ThreadLocalMap 的 key 使用弱引用?
答:
防止 ThreadLocal 对象没有引用时无法被 GC。
Q3:为什么弱引用还会内存泄漏?
答:
因为 key 被回收后变成 null,但是 value 仍然被 Entry 强引用。
Q4:为什么线程池更容易出现问题?
答:
线程池线程长期存在,不会因为请求结束销毁。
Q5:为什么使用 ThreadLocal 后要 remove?
答:
清理 value,避免内存泄漏和数据串线。
⚠️ 工作踩坑
1. 使用 ThreadLocal 不 remove
问题:
内存泄漏
用户数据残留
2. 认为 ThreadLocal 是全局变量
错误:
ThreadLocal 是:
每个线程一份数据
3. 异步线程中直接使用 ThreadLocal
问题:
线程发生变化。
ThreadLocal 数据不会自动传递。
需要:
- 手动传递
- TransmittableThreadLocal 等方案
❓ 自测
-
ThreadLocal 是干什么的?
-
ThreadLocal 数据存在哪里?
-
为什么不用普通变量?
-
ThreadLocalMap 的 key 为什么是弱引用?
-
为什么弱引用还会内存泄漏?
-
为什么线程池环境更容易出现问题?
-
使用 ThreadLocal 后为什么需要 remove?
-
Spring 事务为什么会使用类似 ThreadLocal 的机制?