ThreadLocal 是如何实现的?为什么用 ThreadLocalMap?
结论先行:ThreadLocal 的数据不存在 ThreadLocal 对象里,而是存在每个 Thread 自己的 ThreadLocalMap 里。ThreadLocal 只充当"钥匙":set(v) 就是往当前线程的 map 里塞一条 (this, v),get() 就是拿 this 去查。
1. 结构:数据挂在 Thread 上
Thread 对象
├── threadLocals : ThreadLocalMap ← 每个线程独有
│ ├── Entry[0] key=WeakReference(ThreadLocal A) value=订单上下文
│ ├── Entry[1] key=WeakReference(ThreadLocal B) value=TraceId
│ └── ...
└── inheritableThreadLocals : ThreadLocalMap ← 供父子线程继承
ThreadLocal 本身是弱引用 key,value 是强引用。这个设计决定了它能做到"线程隔离"零锁开销——每个线程只读写自己的 map,天然无竞争。
2. get / set 的核心逻辑
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 取 t.threadLocals
if (map != null) {
map.set(this, value); // key 就是 this
} else {
createMap(t, value); // 首次使用时惰性创建
}
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) return (T) e.value;
}
return setInitialValue(); // 没有则调用 initialValue()
}
两个细节:惰性初始化(第一次 set 或 get 才创建 map)、初始值可覆写(withInitial(Supplier) 就是包装了 initialValue())。
3. 一个线程能放多少个 ThreadLocal
| 环节 | 说明 |
|---|---|
| 数组初始容量 | 16 |
| 扩容阈值 | len * 2 / 3(约 66%) |
| 冲突解决 | 开放寻址:i = key.threadLocalHashCode & (len-1),冲突则 i+1 线性探测 |
| 哈希增量 | 每次新建 ThreadLocal 累加 0x61c88647(黄金分割数),让 key 分布均匀 |
| 扩容 | resize() 双倍扩容并 rehash,顺便清理 key 为 null 的脏 Entry |
// 典型用法:一次请求内的上下文透传
public class UserContext {
private static final ThreadLocal<User> HOLDER = new ThreadLocal<>();
public static void set(User u) { HOLDER.set(u); }
public static User get() { return HOLDER.get(); }
public static void clear() { HOLDER.remove(); } // 必须清理!
}
4. 内存泄漏:弱引用 key + 强引用 value
Thread (长生命周期,如线程池线程)
└── ThreadLocalMap
└── Entry
├── key : WeakReference<ThreadLocal> ── 只被弱引用指着
└── value : 业务对象 ── 强引用,永远不会被回收
ThreadLocal 对象被外部释放后,key 变成 null,但 value 还被 Entry 强引用着;只要线程活着(线程池里线程几乎永生),这个 value 就一直留在堆里。这就是"ThreadLocal 内存泄漏"的全部原因,与弱引用本身无关——弱引用只是让 key 能被回收,value 才是泄漏的主体。
解决办法只有一个:用完必须 remove()。ThreadLocalMap 的 getEntry/set 过程中会顺带清理 key 为 null 的槽位(expungeStaleEntry),但那属于"顺手清理",不能指望。
5. 常见追问
追问一:为什么 key 用弱引用?
为了让 ThreadLocal 对象本身能被 GC 回收,避免"ThreadLocal 泄漏"。但设计者没把 value 也做成弱引用,因为 value 往往就是业务要用的数据,弱引用会导致随时被回收。半弱半强是权衡后的结果,代价是必须手动 remove()。
追问二:ThreadLocal 能跨线程传递吗?
不能,get() 读的是当前线程的 map。跨线程要用 InheritableThreadLocal(仅创建子线程时复制一次)或阿里开源的 TransmittableThreadLocal(配合线程池)。
追问三:线程池里用 ThreadLocal 有什么坑?
线程被复用,上一个任务残留的 value 会被下一个任务读到,造成串数据(比如用户 A 的上下文出现在用户 B 的请求里)。所以必须在 finally 里 remove(),Spring 的 RequestContextHolder 也是这么做的。
追问四:为什么不用 HashMap 而自己实现 ThreadLocalMap?
因为需要弱引用 key、需要线性探测(数据量小,比链表更省内存、缓存友好)、还需要在读写路径上顺带清理脏 Entry。HashMap 的拉链法和扩容策略都不适合这个场景。详见 ThreadLocalMap 与 HashMap 的对比。
小结
ThreadLocal 的实现只有一句话:数据存在 Thread 里,ThreadLocal 只是查表的 key。它换来了无锁的线程隔离,代价是必须手动 remove(),否则在线程池场景下 value 会一直活在堆里。