Skip to content

ThreadLocal 的实现原理及内存泄漏问题

约 1671 字大约 6 分钟

ai随机问

2025-07-25

Java 并发编程中,ThreadLocal的实现原理及内存泄漏问题

1. ThreadLocal是什么?

  1. ThreadLocal 实现线程私有变量工具类,确保线程之间的数据隔离

  2. ThreadLocal 的革新是通过 Thread 类中的 ThreadLocalMap 实现线程私有存储

  3. // 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 什么是强/弱引用?

  1. 引用类型决定对象的生命周期与GC
  2. 强引用:日常编码默认都是强引用,只要对象被强引用关联,GC就不会回收该对象,即使OOM
  3. 弱引用:当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,且是弱引用

  1. ThreadLocal 一般是new出来使用的,例如 ThreadLocal<String> tl = new ThreadLocal<>(); 此时有一个强引用指向了tl
  2. 强引用一直存在,则 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 是强引用,看看会发生什么。

  1. 当 ThreadLocal 实例不再被使用,但线程仍然在运行(假设线程的生命周期很长)
  2. 此时 ThreadLocalMap 会一直持有 key (ThreadLocal实例) 和 value (存储的值),无法被回收
  3. 这些无用的 ThreadLocal 实例 和value 会一直占用内存,造成内存浪费,严重则内存泄露

所以当 ThreadLocalMap 中的 key 是弱引用时。

  1. 当发生GC时,如果 ThreadLocal 已经没有强引用,则 ThreadLocalMap 的 key 会被回收
  2. 那么对应的value呢?虽然value仍存在强引用,但 ThreadLocal 的 get()set()remove() 方法会主动清理key=null的value
  3. 所以也避免了value的内存泄露
  4. 所以弱引用的设计等同于:当ThreadLocal实例不再被使用(无强引用),不会因为key的强引用而滞留内存中

👇 提到了很多强引用失效,那么强引用什么时候失效?

5. 强引用什么时候失效?

  1. 线程的生命周期(新建、就绪、运行、阻塞、终止)本身并不直接决定强引用的回收
  2. 强引用所在的“作用域”决定了它何时被释放
  3. 局部变量的强引用(假设 ThreadLocal tl = new ThreadLocal();
    1. 方法执行完毕后,tl 会被销毁(从栈帧中移除)
    2. 后续发生GC时,tl 指向的对象就会被回收
  4. 成员变量的强引用(假设 private ThreadLocal tl = new ThreadLocal();
    1. tl 的生命周期与类的实例绑定
    2. 当类的实例被销毁,tl的强引用才会消失,同样等待后续GC
  5. 静态变量的强隐隐(假设 public static ThreadLocal tl = new ThreadLocal();
    1. 静态变量属于类,其生命周期与类的生命周期一致(类被加载到 JVM 中,直到类被卸载)
    2. 只要类没有被卸载,静态变量tl的强引用就一直存在

👇 明确了当 ThreadLocal 强引用失效后,会被GC回收,和 ThreadLocalMap 中的 key 消失,那么两者的顺序呢?

6. ThreadLocal 回收的对象和时机

以局部变量 ThreadLocal tl = new ThreadLocal() 举例,当方法执行完毕后

  1. 局部变量tl(强引用)被销毁(栈帧移除)
  2. 发生GC:ThreadLocal对象已无强引用,会被 GC 回收
  3. 回收后,ThreadLocalMap中的 key(弱引用)指向的对象已消失,因此 key 会自动变为null

需要明确的是:key 的失效是 ThreadLocal 对象被 GC 回收后的结果,而非方法结束后立即发生

👇 之所以对顺序存在疑问,是我对 ThreadLocalMap 的归属关系不明白

7. ThreadLocalMap的“归属权”

  1. ThreadLocalMap 并不属于 ThreadLocal,而是属于 Thread
  2. ThreadLocalMap 是 Thread 的成员变量(Thread.threadLocals),每个线程独立持有一个

举个例子是:

  1. ThreadLocal 是 “钥匙”
  2. ThreadLocalMap 是线程的 “储物柜”
  3. 钥匙丢了(ThreadLocal 被回收),储物柜里的对应物品(value)可能变成无人认领的 “垃圾”
  4. 但储物柜本身(ThreadLocalMap)属于线程,和钥匙(ThreadLocal)的生命周期无关

👇 好,ThreadLocalMap不归属ThreadLocal,但我发现 ThreadLocalMap是ThreadLocal的内部类,为什么这么设计?

8. ThreadLocalMap为什么设计成ThreadLocal的内部类?

  1. 为了让它能直接访问ThreadLocal的私有成员(比如threadLocalHashCode用于计算数组索引),同时避免暴露给外部

9. 为什么必须调用remove()

  1. 当 ThreadLocal 被回收(key=null),若线程仍存活,value 会一直被 ThreadLocalMap 强引用,无法回收(形成 “孤儿 value”)
  2. 因此,使用 ThreadLocal 后必须调用remove(),手动删除key-value对,彻底避免 value 的内存泄漏。
public void doSomething() {
    ThreadLocal<String> tl = new ThreadLocal<>();
    try {
        tl.set("data");
        // 业务逻辑
    } finally {
        tl.remove(); // 无论是否异常,都清理value
    }
}