ThreadLocal

ThreadLocal 源码分析

ThreadLocal 让每个线程持有同一个变量的独立副本,互不干扰,常用于存用户上下文、数据库连接、SimpleDateFormat 这类线程不安全对象。
核心思路是存储倒置:值并不放在 ThreadLocal 里,而是放在每个 Thread 自带的 ThreadLocalMap 中,ThreadLocal 实例只充当查这张表的 key。这样线程只读写自己的表,天生无锁。

ThreadLocal 外壳:get / set / remove

代码块JAVA · 108 行收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓, copyright 2025), java.lang.ThreadLocal
public class ThreadLocal<T> {

    private final int threadLocalHashCode = nextHashCode();    // 每个实例出生就领一个哈希,终身不变

    private static final AtomicInteger nextHashCode =
        new AtomicInteger();                                    // 全局发号器,static:所有 ThreadLocal 实例共用一条号段

    private static final int HASH_INCREMENT = 0x61c88647;      // 2^32 × 黄金分割比。等差发号乘开后在 2^n 表里近乎均匀散布

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    protected T initialValue() {
        return null;                                            // 默认初值。要改就子类重写,或用 withInitial 传 Supplier
    }

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    public T get() {
        return get(Thread.currentThread());
    }

    // ... getCarrierThreadLocal 等载体线程方法略

    private T get(Thread t) {
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);        // key 是 this:值挂在线程身上,ThreadLocal 只是查表的钥匙
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T) e.value;
                return result;
            }
        }
        return setInitialValue(t);                              // 表没建 / 没这个 key:初始化,并把初值写进表
    }

    private T setInitialValue(Thread t) {
        T value = initialValue();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);                                // 懒创建:多数线程一辈子用不到 ThreadLocal,不必每个线程都背一张表
        }
        if (this instanceof TerminatingThreadLocal<?> ttl) {
            TerminatingThreadLocal.register(ttl);               // JDK 内部专用:线程退出时需要收尾回调的 ThreadLocal
        }
        if (TRACE_VTHREAD_LOCALS && t == Thread.currentThread() && t.isVirtual()) {
            printStackTrace();                                  // 虚拟线程滥用 ThreadLocal 的排查开关 (jdk.traceVirtualThreadLocals)
        }
        return value;
    }

    public void set(T value) {
        set(Thread.currentThread(), value);
        if (TRACE_VTHREAD_LOCALS && Thread.currentThread().isVirtual()) {
            printStackTrace();
        }
    }

    // ...

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

     public void remove() {
         remove(Thread.currentThread());
     }

    // ...

     private void remove(Thread t) {
         ThreadLocalMap m = getMap(t);
         if (m != null) {
             m.remove(this);                                    // 线程池必调:worker 线程不死,不 remove 值就一直挂在表里
         }
     }

    ThreadLocalMap getMap(Thread t) {
        if (this instanceof TerminatingThreadLocal<T>) {
            return t.terminatingThreadLocals();
        } else {
            return t.threadLocals();                            // 表是 Thread 的字段。"线程隔离"的全部秘密:数据本来就分开存
        }
    }

    void createMap(Thread t, T firstValue) {
        var map = new ThreadLocalMap(this, firstValue);
        if (this instanceof TerminatingThreadLocal<T>) {
            t.setTerminatingThreadLocals(map);
        } else {
            t.setThreadLocals(map);
        }
    }

    // ... createInheritedMap / childValue 供 InheritableThreadLocal 使用,略
}

ThreadLocalMap:弱引用 Entry 与线性探测

代码块JAVA · 111 行收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.ThreadLocal.ThreadLocalMap
    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;                                       // value 是强引用;key 走弱引用。内存泄漏问题全出在这个不对称上

            Entry(ThreadLocal<?> k, Object v) {
                super(k);                                       // key 存进 WeakReference 的 referent,外界不再引用时 GC 可回收
                value = v;
            }
        }

        private static final int INITIAL_CAPACITY = 16;        // 必须是 2 的幂,配合 hash & (len-1) 代替取模

        private Entry[] table;

        private int size = 0;

        private int threshold; // Default to 0

        private void setThreshold(int len) {
            threshold = len * 2 / 3;                            // 负载因子 2/3:开放寻址怕表满,比 HashMap 的 0.75 保守
        }

        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);                 // 环形表:探测到末尾绕回开头
        }

        // ... prevIndex 略

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];                // 建表时必然带着第一个键值对,见 createMap 的懒创建
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.refersTo(key))                   // refersTo (JDK 16+) 只比对不取出 referent,不干扰 GC
                return e;
            else
                return getEntryAfterMiss(key, i, e);            // 快路径单独成小方法,方法体够小 JIT 才好内联
        }

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                if (e.refersTo(key))
                    return e;
                if (e.refersTo(null))
                    expungeStaleEntry(i);                       // 路过发现 key 已被 GC 的死条目,顺手清理:get 也在还债
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;                                        // 撞到 null 槽即确定不存在——线性探测的核心不变式
        }

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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.value = value;                            // 结局一:key 已存在,覆盖
                    return;
                }

                if (e.refersTo(null)) {
                    replaceStaleEntry(key, value, i);           // 结局二:撞上死槽,原地复用,并清掉所在 run 的其他死条目
                    return;
                }
            }

            tab[i] = new Entry(key, value);                     // 结局三:探到 null 槽,新建
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)      // 先试启发式清理;清不出东西且超阈值才 rehash
                rehash();
        }

        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);                       // 复用统一清理逻辑:value 置 null、槽位释放、后继搬移
                    return;
                }
            }
        }

        // ... replaceStaleEntry 略:向前后扫描死条目所在的 run,换入新 Entry 后整段清理
    }

清理与扩容:expunge / cleanSomeSlots / resize

代码块JAVA · 87 行收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.ThreadLocal.ThreadLocalMap
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;                        // 断掉 value 的强引用,"泄漏"的对象在这一行被放生
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            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--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {                               // 挖掉中间的槽会切断探测链,被挤到后面的条目必须搬回来
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.refersTo(null)) {
                    n = len;                                    // 一旦逮到死条目就把扫描配额加大,乘胜追击
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);                         // 只扫 log2(n) 个槽:在"不清理"和"全表扫 O(n)"之间折中
            return removed;
        }

        private void rehash() {
            expungeStaleEntries();                              // 扩容前先全表清死条目,很可能清完就不用扩了

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)              // 清完仍逼近阈值才真扩容,避免反复在阈值边缘抖动
                resize();
        }

        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (Entry e : oldTab) {
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

原理串讲

代码块JAVA · 2 行收起展开
一次 `userHolder.get()` 的完整链路:`get()` 拿到 `Thread.currentThread()` 转给私有的 `get(Thread t)`;`getMap(t)` 直接返回 `t.threadLocals()`,即当前线程对象上的那张 ThreadLocalMap。
表存在就调 `map.getEntry(this)`:用 `threadLocalHashCode & (table.length - 1)` 一步定位槽位,`e.refersTo(key)` 命中立刻返回;不命中转 `getEntryAfterMiss` 沿环形表线性探测,途中每遇到 `refersTo(null)` 的死条目就调 `expungeStaleEntry` 顺手清掉,直到撞见 null 槽宣告不存在。

查不到(或表还没建)就走 setInitialValue(t):调 initialValue() 取初值,再 map.set 写入或 createMap 建表,所以 get 也可能触发写。

代码块JAVA · 2 行收起展开
`set` 这边,`map.set` 的探测循环只有三种结局:`refersTo(key)` 覆盖旧值;
`refersTo(null)` 交给 `replaceStaleEntry` 原地复用死槽并清理整个 run(两个 null 槽之间的连续段);

探到 null 槽新建 Entry,随后 cleanSomeSlots(i, sz) 扫 log2(n) 个槽做启发式清理,一个死条目都没清出来且 size >= threshold(容量的 2/3)才进 rehash()——先 expungeStaleEntries() 全表清一遍,仍然 size >= threshold - threshold / 4resize() 翻倍搬迁。

第一处为什么:为什么把 map 挂在 Thread 上、拿 ThreadLocal 当 key,而不做一张全局的 Map<Thread, T>?全局表意味着所有线程竞争同一个数据结构,读写都要同步;倒置之后每个线程只碰自己的表,get/set 全程零锁零 CAS。
而且线程结束时 Thread 对象被回收,整张表连同所有 value 一起陪葬,生命周期管理是免费的——这正是全局表最难做对的部分。

第二处为什么:为什么哈希用 0x61c88647 等差发号,而不用对象默认 hashCode?ThreadLocal 通常作为 static 字段在类加载时按顺序批量创建,发出来的号是等差数列;乘上黄金分割数(斐波那契散列)再对 2^n 取模,连续的号会被打散到表中彼此远离的槽位,配合线性探测几乎不冲突。
源码注释明说这个散列”只在 ThreadLocalMap 里有用”,它就是为这张小表定制的。

第三处为什么:为什么 key 用弱引用,却不配 ReferenceQueue?弱引用解决的是”外界已经丢弃 ThreadLocal,Entry 却还强引用着它”的死锁面:key 可以被 GC 掉,把泄漏范围从 key+value 整条缩小到 value 一条。
不上 ReferenceQueue 是省掉队列消费的复杂度,改成 get/set/remove 路过时顺手 expungeStaleEntry——代价写在类注释里:只有这张表继续被访问,死条目才有机会被清除。
所以线程池里那条”用完不 remove 就泄漏 + 串数据”的军规,源头就在这里:worker 线程长活,map 不销毁,没人再访问同名 key 时 value 无限期滞留,下个任务还可能读到上个请求的脏值。
remove() 放 finally 里调,一次解决两个问题。

设计取舍

  • 弱引用只兜一半的底:key 可回收,value 仍被 Entry 强引用。指望 GC 防泄漏是误区,remove() 才是正解。
  • ThreadLocal 惯用 static final:实例全局一份没关系,隔离发生在值上——每个线程各有一张表,同一个 key 在不同表里对应不同 value。
  • 开放寻址 + 2/3 负载因子换缓存友好和低内存,代价是删除不能简单置 null,必须 expungeStaleEntry 搬移后继条目维持探测链。
  • InheritableThreadLocal 只在子线程构造时经 createInheritedMap 拷贝一次快照,之后父线程的修改不再传递;线程池的 worker 早已建好,继承基本失效。
  • 虚拟线程(JDK 21+)也支持 ThreadLocal,但百万虚拟线程 × 每线程一张表的成本可观,JDK 25 专门加了 jdk.traceVirtualThreadLocals 排查开关;新代码的方向是 ScopedValue。

延伸阅读