问题

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,配合清理机制可以释放内存

面试答题要点

  1. 核心答案:key是否为null取决于外部是否还持有ThreadLocal的强引用
  2. 常见场景:static字段(强引用)+ Entry弱引用,GC后key不会为null
  3. 弱引用目的:允许ThreadLocal对象被GC,避免永久性内存泄漏
  4. 清理时机:key为null后,ThreadLocalMap在set/get/remove时会清理过期Entry
  5. 最佳实践:使用static修饰ThreadLocal + 及时调用remove(),避免依赖弱引用的清理机制