网站Logo 花果酿的小花园

详解TheadLocal内存泄漏风险

huaguoniang
11
2026-01-25

在学习多线程的过程中,或多或少都听说到ThreadLocal会有内存泄漏的风险,那么具体是为什么?下面来进行详细的解析一下。

提纲:

  1. ThreadLocal是什么

  2. 弱引用

  3. 内存泄漏

  4. 总结

1. ThreadLocal是什么?

public class ThreadLocal<T> {
    //内部类,一个map
    static class ThreadLocalMap {
        //map的具体内容,entry<k,v>  且k是弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            //规定了entry的key是ThreadLocal
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
       private Entry[] table;
    }
}

从源码不难看出:

  1. ThreadLocal有一个内部类ThreadLocalMap

  2. ThreadLocalMap的数据结构就是一个Entry数组

  3. 而一个Entry就是一个Key - Value键值对,以 ThreadLocal为key,以Object为Value的Map

再来看看Thread的源码

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

即:

  1. ThreadLocal对象,挂载在当前线程中,具体存储在Thread中的成员变量ThreadLocalMap threadLocals中

  2. ThreadLocalMap为一个键值对Map,key为ThreadLocal,value为Object

2. 弱引用

从ThreadLocalMap的源码中static class Entry extends WeakReference<ThreadLocal<?>>,可以看到这里的Key(ThreadLocal)使用了弱引用。

弱引用:当对象不再被任何强引用关联、仅被弱引用持有时,只要发生 GC,该对象就会被回收。

为什么要将key设计成弱引用呢?

来看一下创建了一个TheadLocal对象之后的堆栈引用示意图:

a. 堆栈引用示意图

public class ThreadLocal<T> {
            public void set(T value) {
                Thread t = Thread.currentThread();
                ThreadLocalMap map = getMap(t);
                if (map != null) {
                    map.set(this, value);
                } else {
                    createMap(t, value);
                }
            }

            void createMap(Thread t, T firstValue) {
                t.threadLocals = new ThreadLocalMap(this, firstValue);
            }
}

当创建了一个ThreadLocal新对象之后,就会有几个主要的引用:

  1. new ThreadLocal,在堆区中创建一个ThreadLocal对象,线程中的引用指向新创建出来的ThreadLocal对象 A ,如图中“1”

  2. Thread的引用指向线程对象,如图中“2”

  3. 线程对象中有threadLocals 指向ThreadLocalMap对象,如图中“3”

  4. ThreadLocalMap中有指向Entry数组的引用,如图中“4”

  5. 其中每一个entry,包含一个key和一个value。key指向ThreadLocal对象,如图中“5”

当业务使用完毕,不在需要该ThreadLocal对象,如

ThreadLocal t1 = new ThreadLocal();
t1.set(value);
t1 = null;

b. "t1 = null"

  • 线程栈中的“1”引用断开,且当前作用域中没有任何其他强引用指向该 ThreadLocal

  • 这时候ThreadLocal对象,只有一个弱引用“5”

  • 此时一旦发生 GC,该 ThreadLocal 对象就会被回收

c. GC回收

所以,Key为弱引用的原因:

  1. 及时将ThreadLocal对象的空间进行释放,避免内存堆积

  2. 如果是key为强引用,即使t1 = null了,只要线程存活,就会一直存在该ThreadLocal对象

3. 内存泄漏

内存泄漏(Memory Leak)
指对象在逻辑上已经不再被程序使用,但由于仍然存在引用链,导致垃圾回收器无法回收该对象,从而使其长期占用内存。

  • 在 ThreadLocalMap 中,Entry 的 key 是 ThreadLocal 的弱引用,GC 时如果外部不再持有 ThreadLocal 的强引用,key 会被回收并变为 null

  • 但 Entry 中的 value 是强引用,只要 Entry 本身仍然存在,value 对象就始终可达,无法被 GC 回收。

  • 因此,当线程长期存活且未显式调用 ThreadLocal.remove() 时,key 已经被回收但 value 仍被 ThreadLocalMap 持有,形成“逻辑上无用、但 GC 无法回收”的对象,这就是 ThreadLocal 存在内存泄漏风险的根本原因。

别急,ThreadLocal中对此做了处理,算是一种兜底机制:

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.refersTo(key)) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
private int expungeStaleEntry(int staleSlot) {
            ......
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } 
            ......
        }
  1. remove()方法中,会先将key存在的entry进行删除,然后调用expungeStaleEntry()

  2. 在expungeStaleEntry()方法中,会将key为null对应的value进行置空,再将entry置空删除

但是,

  • ThreadLocalMap 的清理是惰性的、被动触发的,只有在再次访问 ThreadLocalMap 时才可能执行(比如remove(),get(),set());

  • 如果线程长期存活但不再访问 ThreadLocalMap,key 为 null 的 Entry 及其 value 将一直滞留,导致内存泄漏,因此必须显式调用 remove()。

  • 我们不能过于依赖这个expungeStaleEntry(),能remove(),还是要remove()。

4. 总结:

  • 因此,ThreadLocal 的内存泄漏风险并非来源于 ThreadLocal 本身

  • 而是由于在线程生命周期较长的场景下,key 被回收而 value 仍被强引用持有所导致。

  • 为了避免 value 泄漏,最佳实践是在使用完 ThreadLocal 后显式调用 remove() 方法

动物装饰