1.代码分析

每个线程类(Thread.class)都会有一个自己的ThreadLocals,即

ThreadLocal.ThreadLocalMap threadLocals = null;

而一个ThreadLocalMap,存储了一个包含Entry对象的数组 table:

        Entry 继承了弱引用 WeakReference --> set时会将key置为弱引用,而value则是强引用。

 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;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

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

我们可以在程序中设置多个ThreadLocal<T> 对象作为key,如

private static final ThreadLocal<AgentContext> threadLocal = new ThreadLocal<>();
private static final ThreadLocal<String> instance = new ThreadLocal();

这些ThreadLocal对象将作为key,用来存储或获取ThreadLocalMap中的entry。其中会通过

int i = key.threadLocalHashCode & (table.length - 1);

为每个key去设置固定的数组下标,在设置或清除时对指定位置的value进行覆盖/清除。

get方法

  • 找到线程的 threadLocals

  • threadLocalHashCode & (len-1) 定位槽位。

  • 找到 Entry 就返回 value。

  • 没找到则调用 initialValue() 并 set 进 map。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);                // 获取当前线程的 ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); // 用 key 查找 Entry
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;                // 返回 value
            return result;
        }
    }
    return setInitialValue();                       // 没有则初始化
}

set方法

  • 覆盖已有 value。

  • 遇到 stale entry 会替换并清理。

  • 如果数组满了,可能扩容。

private void set(ThreadLocal<?> key, Object value) {
    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)) {                    // 找到旧 Entry
            e.value = value;                      // 覆盖 value
            return;
        }
        if (e.refersTo(null)) {                   // 遇到 stale entry
            replaceStaleEntry(key, value, i);    // 替换并清理
            return;
        }
    }

    tab[i] = new Entry(key, value);              // 没有旧 Entry,直接插入
    size++;
    if (!cleanSomeSlots(i, size) && size >= threshold)
        rehash();                                // 扩容或清理
}

remove方法

  • 只清理指定 ThreadLocal 对应的 Entry。

  • 结合 expungeStaleEntry() 避免线程池长期持有 value 导致内存泄漏。

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)) {        // 找到对应 Entry
            e.clear();                // 清空 key
            expungeStaleEntry(i);     // 清理后续 stale entry
            return;
        }
    }
}

2.ThreadLocal 内存占用机制总结(含 static final 情况)

  1. ThreadLocalMap 结构

    • 每个线程维护一个 ThreadLocalMap,存储线程对应的 ThreadLocal 对象。

    • Entrykey 是 ThreadLocal 的弱引用,value 是线程持有的强引用对象(ex:AgentContext)。

  2. 弱引用 key 的作用

    • 当 ThreadLocal 对象本身不再被引用时,弱引用 key 可以被 GC 回收。

    • Entry 会变成 stale entry(key 为 null),但 value 仍存在 map 中。

  3. 潜在的内存占用问题

    • 普通 ThreadLocal:如果线程长期存在(例如线程池线程复用),stale entry 的 value 会长期占用内存。

    • static final ThreadLocal:ThreadLocal 对象本身一直有强引用,不会被回收。

      • key 永远不会变成 null,Entry 永远有效。

      • 因此,如果 value 没有被 remove(),线程存活期间一直被强引用占用。

      • 不会出现 “stale entry” 情况,但线程池长期使用下仍需 remove 避免内存长期占用。

  4. 所以,不管是基于内存泄漏的风险,还是赋值污染的问题,都建议手动进行remove()

    • 使用完 ThreadLocal 后,必须手动调用 remove(),尤其在线程池等长期存活线程中。static final ThreadLocal 虽然不会变成 stale entry,但 remove() 仍是释放 value 内存的必要操作。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐