问题
ThreadLocal的key是弱引用,那么在ThreadLocal.get()的时候发生GC之后key是为null吗?
答案
核心结论
不一定为null,关键在于外部是否还持有ThreadLocal的强引用。
- ✅ 外部有强引用:即使GC,ThreadLocal对象不会被回收,Entry的key不会变为null
- ❌ 外部无强引用:GC后ThreadLocal对象被回收,Entry的key变为null
引用链分析
典型场景的引用关系:
// 场景1:外部有强引用(常见写法)
public class UserContext {
private static ThreadLocal<User> userHolder = new ThreadLocal<>(); // 强引用
public static User get() {
return userHolder.get();
}
}
引用链示意图:
UserContext类(存活)
↓ 强引用
static字段 userHolder
↓ 强引用
ThreadLocal对象 ← 这里有强引用,GC不会回收!
↑ 弱引用
Entry.key (WeakReference)
关键点:
- static字段持有ThreadLocal的强引用
- Entry的key虽然是WeakReference,但ThreadLocal对象仍有强引用
- GC时,ThreadLocal对象不会被回收
- Entry的key不会变为null
场景对比
场景1:外部有强引用(key不会为null)
public class StrongReferenceDemo {
// static变量持有强引用
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
threadLocal.set("测试数据");
// 手动触发GC
System.gc();
Thread.sleep(100);
// 读取值
String value = threadLocal.get();
System.out.println("GC后读取: " + value); // 输出: GC后读取: 测试数据
// key仍然有效
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getThreadLocalMap(t);
// Entry的key仍然指向ThreadLocal对象,不为null
}
}
原因:
static threadLocal变量持有强引用- GC时,ThreadLocal对象有强引用,不会被回收
- Entry的key仍然指向有效的ThreadLocal对象
场景2:外部无强引用(key会为null)
public class WeakReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
// 局部变量,方法执行完后无强引用
ThreadLocal<String> localVar = new ThreadLocal<>();
localVar.set("测试数据");
// 模拟业务逻辑执行
try { Thread.sleep(50); } catch (InterruptedException e) {}
// 方法结束后,localVar出栈,ThreadLocal对象失去强引用
});
thread.start();
thread.join();
// 此时ThreadLocal对象只有Entry的弱引用
System.gc(); // 触发GC
Thread.sleep(100);
// 此时Entry的key已经为null(ThreadLocal对象被回收)
// value仍然存在,导致内存泄漏!
}
}
引用变化过程:
1. 方法执行中:
局部变量localVar --强引用--> ThreadLocal对象
↑ 弱引用
Entry.key
2. 方法执行完毕:
局部变量localVar出栈,强引用消失
ThreadLocal对象 <--弱引用-- Entry.key
3. GC后:
ThreadLocal对象被回收
Entry.key = null
Entry.value仍存在(强引用)← 内存泄漏!
验证实验
使用反射查看Entry的key:
public class VerifyWeakReference {
public static void main(String[] args) throws Exception {
// 创建ThreadLocal并设置值
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("测试数据");
// 获取Thread的threadLocals字段
Thread thread = Thread.currentThread();
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Object threadLocalMap = threadLocalsField.get(thread);
// 获取table字段
Field tableField = threadLocalMap.getClass().getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(threadLocalMap);
// 查找Entry
for (Object entry : table) {
if (entry != null) {
// Entry继承WeakReference,调用get()获取referent
Field referentField = Reference.class.getDeclaredField("referent");
referentField.setAccessible(true);
Object key = referentField.get(entry);
System.out.println("key是否为null: " + (key == null));
System.out.println("key对象: " + key);
// 获取value
Field valueField = entry.getClass().getDeclaredField("value");
valueField.setAccessible(true);
Object value = valueField.get(entry);
System.out.println("value: " + value);
}
}
// 移除强引用
tl = null;
// 触发GC
System.gc();
Thread.sleep(100);
System.out.println("\n--- GC后 ---");
// 再次检查(key可能变为null)
for (Object entry : table) {
if (entry != null) {
Field referentField = Reference.class.getDeclaredField("referent");
referentField.setAccessible(true);
Object key = referentField.get(entry);
System.out.println("key是否为null: " + (key == null));
System.out.println("key对象: " + key);
}
}
}
}
get()时的处理逻辑
ThreadLocal.get()源码:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // this就是外部的强引用
if (e != null) {
return (T)e.value;
}
}
return setInitialValue();
}
关键点:
this是当前ThreadLocal对象的引用- 如果外部有强引用(如static字段),
this指向的对象就是那个被强引用的对象 getEntry(this)查找时,key能匹配成功
getEntry()源码:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key) { // e.get()获取弱引用的对象
return e;
} else {
return getEntryAfterMiss(key, i, e);
}
}
e.get()的含义:
// Entry继承WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// WeakReference.get()返回referent(被弱引用的对象)
public T get() {
return this.referent; // 如果对象被GC,返回null
}
实际案例分析
案例1:Spring框架的RequestContextHolder
public abstract class RequestContextHolder {
// static强引用,ThreadLocal对象不会被GC
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
public static RequestAttributes getRequestAttributes() {
return requestAttributesHolder.get(); // key不会为null
}
}
案例2:错误的局部变量用法
// ❌ 错误示例
public void processRequest() {
ThreadLocal<User> userLocal = new ThreadLocal<>(); // 局部变量
userLocal.set(currentUser);
// 业务处理...
doSomething();
// 方法结束,userLocal出栈
// ThreadLocal对象失去强引用,可能被GC
// Entry的key变为null,但value仍存在(内存泄漏)
}
// ✅ 正确示例
public class UserContext {
private static final ThreadLocal<User> userLocal = new ThreadLocal<>(); // static强引用
public static void set(User user) {
userLocal.set(user);
}
public static void clear() {
userLocal.remove(); // 必须清理
}
}
为什么需要弱引用?
场景假设:如果Entry的key是强引用
// 假设Entry的key是强引用(实际是弱引用)
static class Entry {
ThreadLocal<?> key; // 强引用
Object value;
}
问题:
外部代码:
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("data");
// 业务完成后
tl = null; // 试图释放ThreadLocal对象
引用链:
tl变量(置为null)
✗ 无引用
Thread对象(仍存活)
↓ 强引用
ThreadLocalMap
↓ 强引用
Entry
↓ 强引用(假设)
ThreadLocal对象 ← 无法被GC!
结果:
- 即使外部
tl = null,ThreadLocal对象仍被Entry强引用 - ThreadLocal对象无法被GC回收
- 导致永久性内存泄漏
使用弱引用后:
- 外部
tl = null后,ThreadLocal对象只剩弱引用 - GC时可以回收ThreadLocal对象
- Entry的key变为null,配合清理机制可以释放内存
面试答题要点
- 核心答案:key是否为null取决于外部是否还持有ThreadLocal的强引用
- 常见场景:static字段(强引用)+ Entry弱引用,GC后key不会为null
- 弱引用目的:允许ThreadLocal对象被GC,避免永久性内存泄漏
- 清理时机:key为null后,ThreadLocalMap在set/get/remove时会清理过期Entry
- 最佳实践:使用static修饰ThreadLocal + 及时调用remove(),避免依赖弱引用的清理机制