探究 ThreadLocal 原理

1、ThreadLocal 的基本原理

ThreadLocalset(T value) 方法,会将 value 保存在 Thread 类的 ThreadLocalMap 变量中,而 ThreadLocalMap 又是 ThreadLocal 的内部类:

package java.lang;

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap inheritableThreadLocals;
}

ThreadLocalset(T value) 方法保存专属于线程副本的原理如下:

package java.lang;

public class ThreadLocal<T> {
    ...
    public void set(T value) {
        set(Thread.currentThread(), value);
        if (TRACE_VTHREAD_LOCALS) {
            dumpStackIfVirtualThread();
        }
    }

    private void set(Thread t, T value) {
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        // 注意,这里的 threadLocals 是 Thread 类中 ThreadLocalMap 类型的变量
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    ...
}

ThreadLocalMap 为什么是弱引用:

package java.lang;

public class ThreadLocal<T> {
    ...
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            // 从这句可以看出 ThreadLocalMap 保存的 Entry 对象,key 为 super(k),
            // 即调用父类 WeakReference 的构造方法,形成了弱引用
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        // 注意这里的构造方法,在前文的 set(T value) 位置被调用了
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 这里将 Entry 对象存入了 table 中
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        ...
    }
}

ThreadLocal 中取回存入的对象:

public class ThreadLocal<T> {
    ...
    private T get(Thread t) {
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // !!注意这个 map 是 ThreadLocalMap 类型的
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T) e.value;
                return result;
            }
        }
        return setInitialValue(t);
    }

    static class ThreadLocalMap {
        ...
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            // 还记得这个 table 吗?看看前文,这里保存的是当前线程所有的
            // ThreadLocalMap,因为每个 Thread 实例中都包含一个私有的 ThreadLocalMap
            Entry e = table[i];
            if (e != null && e.refersTo(key))
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
    }
}

2、为什么 Entry 的 key 用弱引用?

如果 key 是强引用,即使你不再使用这个 ThreadLocal 对象(没有任何地方引用它),它依然会被 ThreadLocalMap 中的 Entry 强引用着,此时 ThreadLocal 对象无法被回收。如果该线程是一个线程池中的线程,它迟迟不被销毁,那么它内部的 ThreadLocalMap 也会一直存在,导致 ThreadLocal 以及它绑定的 value 一直无法回收,这就造成了内存泄漏

思考:ThreadLocal 对象即使没被使用,它依然会被 ThreadLocalMap 中的 Entry 强引用着,此时 ThreadLocal 对象无法被回收,这是为什么呢?

因为 ThreadLocalMap 的生命周期和 Thread 相同,所以 ThreadLocalMap 对象不会被回收,因为 Thread 是 GC Root

如果 key 是弱引用:

一旦外部不再引用这个 ThreadLocal,它就会被 GC 掉。虽然 ThreadLocalMap 中的 Entry 还在,但它的 key 已经是 null;接下来,ThreadLocalMap 在后续的 get()set() 操作中,会检测到这些 key 为 null 的条目,并通过内部的清理机制将其移除,以避免内存泄漏。

3、为什么没有调用 remove() 会导致内存泄漏?

当你在使用 ThreadLocal,但没有调用 remove() 时,很容易造成 内存泄漏

1️⃣ 为什么 remove() 是必要的?

如果不调用 remove(),这个 Entry 会一直保留在 ThreadLocalMap 中;即便 ThreadLocal 本身在代码中被丢弃,Key 可能被 GC 清理(成为 stale entry),但value 依然强引用在 map 中,直到强制清理或重建 map 时才释放。在线程池的场景中,线程被复用,当线程长时间空闲时,这些 value 永远不会被收回,最终造成内存堆积。因为清理机制必须依赖对 ThreadLocal 的访问触发

2️⃣ 清理机制的触发条件

ThreadLocalMap 中存在清理机制,比如 expungeStaleEntries() 会在 get()/set() 操作时被调用,但它只有在后续访问 ThreadLocalMap 时触发。如果一个线程执行后不再触碰 ThreadLocal,这些 Entry 也不会主动清除,对应的 value 就会一直占着内存直至线程结束。

This article was updated on