简单聊聊 ThreadLocal 源码

简单聊聊 ThreadLocal 源码

这篇基于 jdk1.8.0_231java/lang/ThreadLocal.java分析。

全 JDK 里没几个类像 ThreadLocal 这样------全类只有一个原子递增的哈希魔数,没有一把锁,却天然线程安全

它不是靠锁,而是靠一个关键设计:数据根本没存在 ThreadLocal 里,而是存在每个线程自己的 Map 里

这篇从 get/set 执行流程切入,把 ThreadLocalMap 聊明白。


一、它到底在解决什么问题

一句话:给每个线程一份独立副本,天然隔离,无锁安全。

经典的线程 ID 例子(源码注释原例):

java 复制代码
private static final AtomicInteger nextId = new AtomicInteger(0);

private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            @Override protected Integer initialValue() {
                return nextId.getAndIncrement();     // 每个线程首次 get 时各拿一个 ID
            }
        };

threadId.get() 每个线程拿到的都是自己的 ID,互不干扰。三个特征:

  1. 线程隔离------每个线程的副本互不可见;
  2. 无锁并发------副本各写各的,不存在共享状态;
  3. 懒初始化 ------首次 get 才通过 initialValue() 产生初值。

面试点:ThreadLocal 的本质是"空间换时间"------用每线程一份副本,换掉锁。

二、核心:数据存在哪?------每个线程自带一个 ThreadLocalMap

最大的误区是"值存在 ThreadLocal 里"。 恰恰相反,ThreadLocal 只是一个 key,真正的值是存在线程对象里的:

java 复制代码
// Thread.java:每个线程自带两个 Map
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;           // 普通 ThreadLocal 放这
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可继承的放这
}

关系是一张链:

复制代码
Thread(线程对象,长期存活)
  └─ threadLocals (ThreadLocalMap,自定义哈希表)
        └─ Entry[] 数组
              ├─ Entry[哈希槽] → key(弱引用 ThreadLocal) → value(值,强引用)
              └─ ...

ThreadLocalMap 是 ThreadLocal 的私有静态内部类,外部不可见,只在 ThreadLocal 内部使用:

java 复制代码
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;                            // 值:强引用
        Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }
    }
    private Entry[] table;                        // 哈希桶(开放寻址,不是链表)
    private int size = 0;
    private int threshold;                        // 扩容阈值
}

所以:

  • set 的本质是 this(ThreadLocal) → map.set(this, value);
  • get 的本质是 map.getEntry(this),找不到才初始化;
  • Map 是懒创建 的:第一次 set/getcreateMap 才建(ThreadLocalMap(firstKey, firstValue),初始容量 16)。

三、Entry 为什么用 WeakReference(弱引用)

Entry extends WeakReference<ThreadLocal<?>>------key 是弱引用,value 是强引用。这是整个设计最精妙也最容易出问题的地方。

对比一下强/弱两种设计:

key 引用类型 Thread → Map → Entry → key 后果
强引用 线程活着,ThreadLocal 实例就死不掉 ThreadLocal 对象泄漏(即使业务早不用了)
弱引用(JDK 采用) ThreadLocal 没有强引用了 → 被 GC → key 变 null ThreadLocal 不泄漏,但 value 还挂在 Entry 上

弱引用解决的是"ThreadLocal 对象本身不泄漏 ":ThreadLocal 通常是 static 字段,但也有动态创建的,弱引用保证业务不再引用它时能被回收。代价是------value 是强引用,一旦 key 变 null,这个 Entry 就成了"过期条目"(stale entry),值还占着内存,等被清理。

面试点:弱引用防的是 key 泄漏,防不了 value 泄漏。内存泄漏的根子在 value 上,见第八节。

四、哈希:一个魔数 + 开放寻址

魔数 0x61c88647

java 复制代码
private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;    // 黄金分割常数

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);       // 原子递增
}
  • 每个 ThreadLocal 对象构造时拿一个递增的哈希码,步长是固定魔数 0x61c88647(黄金分割比例 2^32 × (√5−1)/2 的取整);
  • 用它做步长,相邻创建的 ThreadLocal 哈希码在 2 的幂长度的表上分布近乎均匀------所以正常场景下每个 key 都能直接命中自己的槽,碰撞极少。

开放寻址 + 线性探测

ThreadLocalMap 不用 HashMap 的链表/红黑树,用开放寻址(数组里找空位),冲突时向前/后线性探测:

java 复制代码
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }

为什么不用链表?工程权衡,三个理由:

  1. 存储量小:一个线程的 ThreadLocal 通常就几个,数组比链表省内存、对缓存友好;
  2. 分布好:魔数保证哈希均匀,碰撞少,开放寻址够用;
  3. 清理方便 :key 被 GC 后槽位会变 null,开放寻址配合"重哈希补洞"能顺带清理过期条目,链表法对大量 null 节点反而更麻烦。

扩容阈值 2/3

java 复制代码
private void setThreshold(int len) { threshold = len * 2 / 3; }   // 负载因子 2/3
private void rehash() {
    expungeStaleEntries();                              // 先全表清一遍过期条目
    if (size >= threshold - threshold / 4)              // 用更低阈值判断是否真扩容
        resize();                                       // 防抖动(hysteresis)
}

五、get() 执行流程

java 复制代码
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);                // ① 从"当前线程"身上取 map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);  // ② 找本 ThreadLocal 的条目
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;                         // ③ 直接返回
        }
    }
    return setInitialValue();                      // ④ 没有 → 初始化
}

map.getEntry两段式:快路径 + 慢路径

java 复制代码
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);  // 定位槽
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;                                          // 快路径:直接命中
    return getEntryAfterMiss(key, i, e);                   // 慢路径:线性探测
}

getEntryAfterMiss 线性探测,遇到 null 槽就停,途中看到过期条目顺手 expungeStaleEntry 清理。

setInitialValue(get 未命中的兜底):

java 复制代码
private T setInitialValue() {
    T value = initialValue();        // 钩子方法,默认返回 null,可重写
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) map.set(this, value);   // map 已存在 → 塞进去
    else createMap(t, value);                // map 不存在 → 建 map
    return value;
}

画成流程:

复制代码
        get()
          │
    取当前线程 t 的 threadLocals map
          │
   ┌──────▼───────┐
   │ map 为 null?  │──是──▶ setInitialValue() → initialValue() → createMap ══▶ 返回
   └──────┬───────┘
          │否
   map.getEntry(this) 直接命中? ──是──▶ 返回 value
          │否
   getEntryAfterMiss 线性探测
          ├─ 找到 → 返回 value
          ├─ 遇到过期条目 → expungeStaleEntry 清理后继续
          └─ 遇到 null 槽 → 没找到 → setInitialValue()

面试点:initialValue() 什么时候执行?首次 get 且之前没 set 过 ;remove() 之后再次 get 会再执行一次。

六、set() 执行流程

java 复制代码
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) map.set(this, value);   // 有 map → 写
    else createMap(t, value);                // 没 map → 建 map(首次 set)
}

ThreadLocalMap.set(核心,注意它没有快路径,注释说"set 创建和覆盖一样常见,快路径反而常失败"):

java 复制代码
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)]) {  // 线性探测
        ThreadLocal<?> k = e.get();
        if (k == key) { e.value = value; return; }        // ① key 已存在 → 覆盖
        if (k == null) {                                  // ② key 已 GC(过期槽)
            replaceStaleEntry(key, value, i);             //    复用这个槽,不浪费
            return;
        }
    }
    tab[i] = new Entry(key, value);                       // ③ 找到空槽 → 新 Entry
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)        // ④ 没清出空间且超阈值 → 扩容
        rehash();
}

replaceStaleEntry 是复用过期槽的关键:向后找到真正的 key 就交换位置 保持哈希表顺序,找不到就把新值放进过期槽,再顺带清理整个 run(两个 null 槽之间的一段)里的其他过期条目。

七、remove() 与过期条目的清理

java 复制代码
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) m.remove(this);
}

map.remove 找到条目后:

java 复制代码
if (e.get() == key) {
    e.clear();               // ① 清掉 WeakReference 里的引用
    expungeStaleEntry(i);    // ② 清理本 run 的过期条目
    return;
}

过期条目不会自动消失,只会在特定时机被清:

  • get 慢路径 遇过期 → expungeStaleEntry;
  • set 遇过期 → replaceStaleEntry;插入后 cleanSomeSlots(对数扫描);
  • rehash/resizeexpungeStaleEntries 全表清扫。

源码注释说得很直白:"没有用引用队列,过期条目只在表快满时才保证被清"------所以清理是"尽力而为",不是即时保证。

八、内存泄漏:为什么线程池里必须 remove()

这是 ThreadLocal 面试的必考灵魂题

泄漏链路:

复制代码
线程池的核心线程长期存活
  └─ Thread.threadLocals (map) 一直活着
        └─ Entry[] 里的 value 是强引用
              └─ 只要不 remove / 不触发清理,value 永远不被 GC

具体场景:

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(10);   // 线程永不回收
for (...) {
    pool.execute(() -> {
        ThreadLocal<BigObject> tl = new ThreadLocal<>();   // 任务里动态创建
        tl.set(new BigObject(...));                        // 大对象挂进线程的 map
        // ... 用完之后,没有 remove
    });
}
  • 任务跑完,tl 这个 ThreadLocal 没了强引用 → key 被 GC 变 null ,但 BigObject 还被线程的 map 强引用着;
  • 线程池线程不死 → map 不死 → 大对象一直占内存,直到清理被触发(且不一定触发);
  • 结果:内存缓慢泄漏,甚至 OOM

正确姿势:

java 复制代码
try {
    tl.set(...);
    ...业务...
} finally {
    tl.remove();        // 用完必 remove,保证进不了泄漏链路
}

面试点:为什么弱引用"防不了"泄漏?因为弱引用只作用于 key,value 是强引用;泄漏的根因是"线程长寿 + value 强引用",不是弱引用本身。

九、配套迷你版:MiniThreadLocal 演示

和线程池 / ConcurrentHashMap 两篇一个套路------上面的机制我写成了可运行的迷你版 `MiniThreadLocal`

弱引用 Entry、开放寻址、魔数、remove、泄漏全部手写,四个演示对应四个面试点:

完整源码

java 复制代码
package com.interview.thread;

import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 迷你版 ThreadLocal ------ 复刻 JDK 1.8 的核心机制:每线程一个开放寻址哈希表,Entry 弱引用 key。
 *
 * 与 JDK 的对应关系(详解见 md/简单聊聊ThreadLocal源码.md):
 *  - 每个线程一个 ThreadLocalMap,key 是 ThreadLocal,value 强引用;
 *  - Entry extends WeakReference<MiniThreadLocal<?>> ------ 弱引用 key,value 强引用;
 *  - 哈希:魔数 0x61c88647 原子递增,开放寻址线性探测;
 *  - 阈值 2/3,rehash 先全表清过期,size >= threshold - threshold/4 才 resize;
 *  - get 快路径 + getEntryAfterMiss 慢路径;set 覆盖/复用过期槽/新条目;remove 清引用 + expunge。
 *
 * 有意识的简化(面试要能诚实说出来):
 *  - JDK 把 map 存在 Thread.threadLocals 字段里;这里用一个 JDK ThreadLocal 做"线程 → map"
 *    的载体,效果等价(每线程一份),代码更短;
 *  - replaceStaleEntry 简化成"直接把新值放进过期槽",省略了向后找 key 交换以保持哈希序的步骤;
 *  - 没有 InheritableThreadLocal(继承)、没有 withInitial(Supplier)、没有 cleanSomeSlots 的
 *    完整阈值语义(这里按 JDK 原样实现了对数扫描)。
 */
public class MiniThreadLocal<T> {

    /** 相邻 ThreadLocal 的哈希步长 ------ 黄金分割常数,让哈希在 2 的幂长度的表上分布均匀 */
    private static final int HASH_INCREMENT = 0x61c88647;
    private static final AtomicInteger nextHashCode = new AtomicInteger();
    private final int threadLocalHashCode = nextHashCode.getAndAdd(HASH_INCREMENT);

    /** 每线程一个 map 的载体:JDK ThreadLocal(值就是下面的 MiniThreadLocalMap) */
    private static final ThreadLocal<MiniThreadLocalMap> HOLDER = new ThreadLocal<>();

    /** 取当前线程的 map,没有则建(懒创建) */
    static MiniThreadLocalMap getMap() {
        MiniThreadLocalMap map = HOLDER.get();
        if (map == null) {
            map = new MiniThreadLocalMap();
            HOLDER.set(map);
        }
        return map;
    }

    /** 钩子方法:首次 get 且之前没 set 时返回的初值,默认 null(JDK 原样) */
    protected T initialValue() {
        return null;
    }

    @SuppressWarnings("unchecked")
    public T get() {
        MiniThreadLocalMap map = HOLDER.get();
        if (map != null) {
            Entry e = map.getEntry(this);
            if (e != null) {
                return (T) e.value;              // 命中直接返回
            }
        }
        return setInitialValue();                // 没有 → 初始化
    }

    private T setInitialValue() {
        T value = initialValue();
        getMap().set(this, value);
        return value;
    }

    public void set(T value) {
        getMap().set(this, value);
    }

    public void remove() {
        MiniThreadLocalMap map = HOLDER.get();
        if (map != null) map.remove(this);
    }

    // ==================== 内部:开放寻址哈希表 ====================

    /** 桶条目:key 弱引用,value 强引用 ------ 这就是"弱引用防 key 泄漏,强引用造成 value 泄漏"的根源 */
    static class Entry extends WeakReference<MiniThreadLocal<?>> {
        Object value;

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

    static class MiniThreadLocalMap {
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table = new Entry[INITIAL_CAPACITY];
        private int size = 0;
        private int threshold = INITIAL_CAPACITY * 2 / 3;   // 负载因子 2/3

        private int hash(MiniThreadLocal<?> key) {
            return key.threadLocalHashCode & (table.length - 1);
        }

        private int nextIndex(int i) {
            return (i + 1 < table.length) ? i + 1 : 0;      // 线性探测,环绕
        }

        Entry getEntry(MiniThreadLocal<?> key) {
            int i = hash(key);
            Entry e = table[i];
            if (e != null && e.get() == key) return e;      // 快路径:直接命中
            return getEntryAfterMiss(key, i, e);            // 慢路径:线性探测
        }

        private Entry getEntryAfterMiss(MiniThreadLocal<?> key, int i, Entry e) {
            while (e != null) {
                MiniThreadLocal<?> k = e.get();
                if (k == key) return e;
                if (k == null) expungeStaleEntry(i);        // 顺手清理过期条目
                else i = nextIndex(i);
                e = table[i];
            }
            return null;                                    // 遇到 null 槽 → 没有
        }

        void set(MiniThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int i = hash(key);
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i)]) {
                MiniThreadLocal<?> k = e.get();
                if (k == key) { e.value = value; return; }  // ① 覆盖
                if (k == null) {                            // ② 过期槽 → 复用
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);                 // ③ 空槽 → 新条目
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)  // ④ 没清出空间且超阈值 → 扩容
                rehash();
        }

        void remove(MiniThreadLocal<?> key) {
            int i = hash(key);
            for (Entry e = table[i]; e != null; e = table[i = nextIndex(i)]) {
                if (e.get() == key) {
                    e.clear();                              // 清掉弱引用里的 key
                    expungeStaleEntry(i);                   // 顺带清理本 run 的过期条目
                    return;
                }
            }
        }

        /** 简化版:新值直接放进过期槽(JDK 会先向后找 key 交换保持哈希序) */
        private void replaceStaleEntry(MiniThreadLocal<?> key, Object value, int staleSlot) {
            table[staleSlot].value = null;                  // 帮助 GC 旧的 value
            table[staleSlot] = new Entry(key, value);
        }

        /** 清掉 staleSlot 的过期条目,并把该 run 里后面的条目重哈希补洞(JDK 同款,Knuth 6.4) */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            Entry e;
            int i;
            for (i = nextIndex(staleSlot); (e = tab[i]) != null; i = nextIndex(i)) {
                MiniThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = hash(k);
                    if (h != i) {
                        tab[i] = null;
                        while (tab[h] != null) h = nextIndex(h);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        /** 对数扫描,找过期条目(JDK 同款:扫描 log2(n) 个槽,发现一个就多清一段) */
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            do {
                i = nextIndex(i);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = table.length;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ((n >>>= 1) != 0);
            return removed;
        }

        /** 先全表清过期,仍超过 threshold - threshold/4 才真正扩容(防抖动) */
        private void rehash() {
            for (int j = 0; j < table.length; j++) {
                Entry e = table[j];
                if (e != null && e.get() == null) expungeStaleEntry(j);
            }
            if (size >= threshold - threshold / 4) resize();
        }

        private void resize() {
            Entry[] oldTab = table;
            int newLen = oldTab.length * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            for (Entry e : oldTab) {
                if (e == null) continue;
                MiniThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;                         // 丢过期
                } else {
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null) h = (h + 1 < newLen) ? h + 1 : 0;
                    newTab[h] = e;
                    count++;
                }
            }
            table = newTab;
            threshold = newLen * 2 / 3;
            size = count;
        }

        // ===== 演示辅助:统计 live / stale =====
        int liveCount() {
            int c = 0;
            for (Entry e : table) if (e != null && e.get() != null) c++;
            return c;
        }

        int staleCount() {
            int c = 0;
            for (Entry e : table) if (e != null && e.get() == null) c++;
            return c;
        }
    }

    // ==================== 演示 ====================
    public static void main(String[] args) throws Exception {
        System.out.println("==== 0. 魔数哈希:相邻 ThreadLocal 分布均匀 ====");
        MiniThreadLocal<String> a = new MiniThreadLocal<>();
        MiniThreadLocal<String> b = new MiniThreadLocal<>();
        MiniThreadLocal<String> c = new MiniThreadLocal<>();
        System.out.printf("HASH_INCREMENT=0x%x, 槽位(表长16): a=%d, b=%d, c=%d%n",
                HASH_INCREMENT, a.threadLocalHashCode & 15, b.threadLocalHashCode & 15, c.threadLocalHashCode & 15);

        System.out.println("\n==== 1. 线程隔离 ====");
        MiniThreadLocal<String> user = new MiniThreadLocal<>();
        user.set("main-user");
        System.out.println("main 线程 get = " + user.get());
        Thread child = new Thread(() -> System.out.println("子线程  get = " + user.get() + "  ← 隔离,拿不到 main 的值"));
        child.start();
        child.join();

        System.out.println("\n==== 2. set / get / remove ====");
        user.set("v2");
        System.out.println("set(\"v2\") 后 get = " + user.get());
        user.remove();
        System.out.println("remove() 后 get = " + user.get() + "  ← 已清空");

        System.out.println("\n==== 3. 线程池泄漏场景(重点) ====");
        ExecutorService pool = Executors.newFixedThreadPool(1);   // 核心线程永不回收
        Future<?> f1 = pool.submit(() -> {
            MiniThreadLocal<byte[]> tl = new MiniThreadLocal<>();
            tl.set(new byte[1024 * 1024]);                        // 1MB,用后不 remove
            System.out.println("任务1: set 1MB 后  live=" + getMap().liveCount()
                    + ", stale=" + getMap().staleCount());
        });                                                        // tl 出作用域,再无强引用
        f1.get();

        for (int i = 0; i < 3; i++) { System.gc(); Thread.sleep(100); }   // 让弱引用 key 被回收

        Future<?> f2 = pool.submit(() -> {
            System.out.println("任务2(同线程) GC 后: live=" + getMap().liveCount()
                    + ", stale=" + getMap().staleCount() + "  ← key 已回收但 value 仍在 = 内存泄漏");
            // 正确姿势:try-finally remove
            int baseLive = getMap().liveCount(), baseStale = getMap().staleCount();
            MiniThreadLocal<byte[]> ok = new MiniThreadLocal<>();
            try {
                ok.set(new byte[16]);
                System.out.println("正确姿势 set 后   live=" + getMap().liveCount() + " (此条目标记入表)");
            } finally {
                ok.remove();
            }
            System.out.println("正确姿势 remove 后 live=" + getMap().liveCount() + "(回基线 " + baseLive
                    + "), stale=" + getMap().staleCount() + "  ← 用完即清,无泄漏");
        });
        f2.get();
        pool.shutdown();
        System.out.println("\n(演示结束:线程池关闭,线程消亡,残留 map 整体可回收)");
    }
}
复制代码
==== 0. 魔数哈希 ====
HASH_INCREMENT=0x61c88647, 槽位(表长16): a=0, b=7, c=14   ← 相邻 ThreadLocal 均匀散开
==== 1. 线程隔离 ====
main 线程 get = main-user
子线程  get = null  ← 隔离,拿不到 main 的值
==== 2. set / get / remove ====
set("v2") 后 get = v2
remove() 后 get = null  ← 已清空
==== 3. 线程池泄漏场景(重点) ====
任务1: set 1MB 后  live=1, stale=0
任务2(同线程) GC 后: live=0, stale=1  ← key 已回收但 value 仍在 = 内存泄漏
正确姿势 set 后   live=1 (此条目标记入表)
正确姿势 remove 后 live=0(回基线 0), stale=1  ← 用完即清,无泄漏
场景 演示内容 对应小节
0 魔数让相邻 ThreadLocal 槽位均匀(0 / 7 / 14) 四、哈希
1 子线程 get 到 null,拿不到 main 的值 一、线程隔离
2 set / get / remove 语义 五、六、七
3 GC 后 key 回收、value 还在 = 泄漏;remove 防新泄漏 八、内存泄漏

场景 3 是灵魂,一行行看:

  • 任务 1 设了 1MB 不 remove → 输出 live=1, stale=0;
  • 主线程 System.gc() 后,任务 1 的 MiniThreadLocal 已无强引用 → 弱引用 key 被回收 → 输出 live=0, stale=1------value 还挂在线程的 map 上,这就是泄漏实锤;
  • 正确姿势 try-finally remove → 新条目标记入表(live=1)后立即清掉(回基线 0);
  • 注意最后 stale=1 还在------那是任务 1 已经泄漏的旧账,正确代码只能防新泄漏、清不掉旧条目,所以"用完必 remove"要在源头做。

诚实说明(和 JDK 的差距) :载体用 JDK ThreadLocal(JDK 是放 Thread 的 threadLocals 字段,效果等价);replaceStaleEntry 简化成"新值直接占过期槽",省略向后找 key 交换保持哈希序;没有 InheritableThreadLocal / withInitial

十、实战要点与常见用法

常见用法:

场景 例子
线程不安全的工具类 SimpleDateFormat、随机数
一次请求的上下文 事务/连接、当前用户、租户
链路追踪 traceId 透传、MDC 日志
框架数据源 MyBatis-Plus 分页、ShardingSphere、Spring 的 RequestContextHolder

四个坑:

  1. 线程池必 remove:复用线程导致"上一个任务的值残留" = 脏数据,还可能内存泄漏;
  2. 别存大对象:值长期挂在长寿线程上,避免把整个对象图塞进 ThreadLocal;
  3. InheritableThreadLocal 别配线程池 :它只在 Thread 构造时 把父线程的值复制给子线程;线程池是复用线程、不是新建线程,根本不会继承------你会读到上一个任务残留的值,且没人自动清;
  4. 值要不可变:多线程读同一份初始值时要小心共享可变状态(除非每次 set 新对象)。

延伸 (可作加分项):Netty 的 FastThreadLocal 把存储改成数组下标索引 ,ThreadLocal 对象直接持有下标,get/set 是 O(1) 数组访问,省掉哈希和探测,专门服务高频场景。JDK 21 还加了 ThreadLocal.removeAll() 用于整表清理。

十一、面试高频追问

Q1:ThreadLocalMap 为什么用开放寻址,不用链表/红黑树?

一个线程的 ThreadLocal 数量少、哈希靠魔数分布均匀,开放寻址够用且更省内存、对缓存友好;更重要的是过期条目清理方便------key 变 null 的槽配合 expunge 重哈希能顺带补洞,链表法面对大量 null 节点反而麻烦。

Q2:弱引用为什么还会内存泄漏?

弱引用只管 key。value 是强引用,挂在长寿线程(线程池)的 map 上,不 remove 就永远在;清理只在 get/set/remove 时"尽力而为",不是即时保证。

Q3:线程池 + ThreadLocal 的脏数据是怎么来的?

线程复用 → 上一个任务 set 的值没 remove → 下一个任务 get 直接命中残留值 → 读到别人的数据。所以要么 finally remove,要么每次先 set。

Q4:initialValue() 会执行几次?

首次 get 且之前没 set 时执行一次;remove() 之后再次 get 会重新执行。所以别把"只该执行一次的初始化"放进 initialValue。

Q5:父子线程怎么传值?

InheritableThreadLocal。但只在子线程创建(Thread 构造)时复制一次,线程池场景无效,且不会自动清理。

Q6:为什么说 ThreadLocal 无锁?

它的隔离靠"每线程一份副本",数据不共享,自然无锁;如果有共享可变状态,那是用法问题,不是 ThreadLocal 的问题。

结语

ThreadLocal 整个类加在一起,没有一把锁。它的并发安全来自一个巧妙的存放大挪移:

复制代码
值不存 ThreadLocal 里
  而是存在"线程自己的 ThreadLocalMap"里
    key 用弱引用防 ThreadLocal 对象泄漏
    value 用强引用,用完必须 remove

配上魔数 0x61c88647 的均匀哈希、开放寻址的线性探测、2/3 阈值的扩容,和"尽力而为"的过期清理,就是全部。背结论容易,把这五块(存哪、弱引用、魔数、探测、泄漏)讲清楚,才是真懂 ThreadLocal。

相关推荐
论迹复利17 小时前
FreeRTOS 在 RISC-V 上是如何“点火“的 —— 从 main() 到第一个任务的完整链路
java·开发语言·risc-v
张宇Joaquin18 小时前
鲲鹏统一并行加速库KUPL--众核并行能力介绍
java·开发语言·网络
宸津-代码粉碎机18 小时前
AI攻防战升级!基于Spring AI构建Java应用自动免疫安全体系
java·大数据·开发语言·人工智能·python·安全·spring
2601_9637491019 小时前
越华环保集团数字化污水治理:端边云采集架构与平台对接实现
java·大数据·架构
xcLeigh20 小时前
Go入门:整数类型的溢出与安全边界
java·安全·golang
源码宝20 小时前
SpringBoot+Vue2 诊所门诊管理系统,完整前后端源码,可直接部署上线
java·源码·his系统·门诊系统·程序代码·诊所系统·门诊his
yychen_java20 小时前
九:Text-to-SQL 智能数据查询与 Human-in-the-Loop 人机协作
java·人工智能·架构
lhldsg1 天前
智慧场馆解决方案小程序开发实战:从架构设计到部署指南
java·小程序·uni-app
Freak嵌入式1 天前
Pico UART 数据收发实战:硬件配置、MicroPython 编程
java·开发语言·单片机·嵌入式硬件·生活
devilnumber1 天前
Oracle 与 MySQL substr 函数差异总结
java·数据库·mysql·oracle