ThreadLocal 的实现原理及内存泄漏问题
Java 并发编程中,ThreadLocal的实现原理及内存泄漏问题
1. ThreadLocal是什么?
ThreadLocal 实现线程私有变量工具类,确保线程之间的数据隔离
ThreadLocal 的革新是通过 Thread 类中的 ThreadLocalMap 实现线程私有存储
// Thread 类源码片段 public class Thread implements Runnable { // 每个线程都有一个 ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals = null; } // ThreadLocal 类中的内部类 ThreadLocalMap static class ThreadLocalMap { // 存储键值对的数组,类似 HashMap,但结构更简单 private Entry[] table; // Entry 是键值对载体,键是 ThreadLocal 对象(弱引用),值是线程私有变量 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // 线程私有变量(强引用) Entry(ThreadLocal<?> k, Object v) { super(k); // 键 k 被包装为弱引用 value = v; } } }
👇 通过 ThreadLocal 引申出强引用/弱引用概念,什么是强/弱引用?
2 什么是强/弱引用?
- 引用类型决定对象的生命周期与GC
- 强引用:日常编码默认都是强引用,只要对象被强引用关联,GC就不会回收该对象,即使OOM
- 弱引用:当GC发生,对象仅被弱引用关联,就会被立即回收
// 创建一个对象,变量obj就是强引用
Object obj = new Object();
// 即使将obj赋值为null,只要其他地方还有强引用指向该对象,它仍不会被回收
Object anotherObj = obj;
obj = null; // 此时anotherObj仍强引用该对象,对象不会被GC
// 创建一个对象,并用弱引用关联它
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
// 断开强引用(此时对象仅被弱引用关联)
obj = null;
// 手动触发GC(实际中无需手动调用,仅为演示)
System.gc();
// 此时弱引用关联的对象已被回收,get()会返回null
System.out.println(weakRef.get()); // 输出:null
👇 既然 ThreadLocal 对象是弱引用,如果GC发生之前,已经有threadLocal.set("xxx"),GC发生之后,threadLocal.get() 会失败吗?
3. GC发生之后 threadLocal.get 会失败吗?
问出上面的问题,显然是没有搞懂什么使用ThreadLocal,仅仅是看到了ThreadLocalMap中的key是ThreadLocal,且是弱引用
- ThreadLocal 一般是new出来使用的,例如
ThreadLocal<String> tl = new ThreadLocal<>();
此时有一个强引用指向了tl
- 强引用一直存在,则 ThreadLocalMap 中的 key 一直会存在,则 get() 方法不会失败
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
👇 如果强弱引用初步了解了,那ThreadLocalMap中的key设计成弱引用的意义是什么?
4. ThreadLocalMap中的key设计成弱引用的意义是什么?
不妨假设 ThreadLocalMap 中的 key 是强引用,看看会发生什么。
- 当 ThreadLocal 实例不再被使用,但线程仍然在运行(假设线程的生命周期很长)
- 此时 ThreadLocalMap 会一直持有 key (ThreadLocal实例) 和 value (存储的值),无法被回收
- 这些无用的 ThreadLocal 实例 和value 会一直占用内存,造成内存浪费,严重则内存泄露
所以当 ThreadLocalMap 中的 key 是弱引用时。
- 当发生GC时,如果 ThreadLocal 已经没有强引用,则 ThreadLocalMap 的 key 会被回收
- 那么对应的value呢?虽然value仍存在强引用,但 ThreadLocal 的
get()
、set()
、remove()
方法会主动清理key=null的value - 所以也避免了value的内存泄露
- 所以弱引用的设计等同于:当ThreadLocal实例不再被使用(无强引用),不会因为key的强引用而滞留内存中
👇 提到了很多强引用失效,那么强引用什么时候失效?
5. 强引用什么时候失效?
- 线程的生命周期(新建、就绪、运行、阻塞、终止)本身并不直接决定强引用的回收
- 强引用所在的“作用域”决定了它何时被释放
- 局部变量的强引用(假设
ThreadLocal tl = new ThreadLocal();
- 方法执行完毕后,tl 会被销毁(从栈帧中移除)
- 后续发生GC时,tl 指向的对象就会被回收
- 成员变量的强引用(假设
private ThreadLocal tl = new ThreadLocal();
- tl 的生命周期与类的实例绑定
- 当类的实例被销毁,tl的强引用才会消失,同样等待后续GC
- 静态变量的强隐隐(假设
public static ThreadLocal tl = new ThreadLocal();
- 静态变量属于类,其生命周期与类的生命周期一致(类被加载到 JVM 中,直到类被卸载)
- 只要类没有被卸载,静态变量
tl
的强引用就一直存在
👇 明确了当 ThreadLocal 强引用失效后,会被GC回收,和 ThreadLocalMap 中的 key 消失,那么两者的顺序呢?
6. ThreadLocal 回收的对象和时机
以局部变量 ThreadLocal tl = new ThreadLocal()
举例,当方法执行完毕后
- 局部变量
tl
(强引用)被销毁(栈帧移除) - 发生GC:
ThreadLocal
对象已无强引用,会被 GC 回收 - 回收后,
ThreadLocalMap
中的 key(弱引用)指向的对象已消失,因此 key 会自动变为null
需要明确的是:key 的失效是 ThreadLocal 对象被 GC 回收后的结果,而非方法结束后立即发生
👇 之所以对顺序存在疑问,是我对 ThreadLocalMap 的归属关系不明白
7. ThreadLocalMap的“归属权”
- ThreadLocalMap 并不属于 ThreadLocal,而是属于 Thread
- ThreadLocalMap 是 Thread 的成员变量(
Thread.threadLocals
),每个线程独立持有一个
举个例子是:
- ThreadLocal 是 “钥匙”
- ThreadLocalMap 是线程的 “储物柜”
- 钥匙丢了(ThreadLocal 被回收),储物柜里的对应物品(value)可能变成无人认领的 “垃圾”
- 但储物柜本身(ThreadLocalMap)属于线程,和钥匙(ThreadLocal)的生命周期无关
👇 好,ThreadLocalMap不归属ThreadLocal,但我发现 ThreadLocalMap是ThreadLocal的内部类,为什么这么设计?
8. ThreadLocalMap为什么设计成ThreadLocal的内部类?
- 为了让它能直接访问
ThreadLocal
的私有成员(比如threadLocalHashCode
用于计算数组索引),同时避免暴露给外部
9. 为什么必须调用remove()
- 当 ThreadLocal 被回收(key=null),若线程仍存活,value 会一直被 ThreadLocalMap 强引用,无法回收(形成 “孤儿 value”)
- 因此,使用 ThreadLocal 后必须调用
remove()
,手动删除key-value
对,彻底避免 value 的内存泄漏。
public void doSomething() {
ThreadLocal<String> tl = new ThreadLocal<>();
try {
tl.set("data");
// 业务逻辑
} finally {
tl.remove(); // 无论是否异常,都清理value
}
}