ThreadLocal
ThreadLocal 源码分析
ThreadLocal 让每个线程持有同一个变量的独立副本,互不干扰,常用于存用户上下文、数据库连接、SimpleDateFormat 这类线程不安全对象。
核心思路是存储倒置:值并不放在 ThreadLocal 里,而是放在每个 Thread 自带的 ThreadLocalMap 中,ThreadLocal 实例只充当查这张表的 key。这样线程只读写自己的表,天生无锁。
ThreadLocal 外壳:get / set / remove
代码块收起展开
// 基于 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 与线性探测
代码块收起展开
// 基于 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
代码块收起展开
// 基于 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;
}原理串讲
代码块收起展开
一次 `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 也可能触发写。
代码块收起展开
`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 / 4 才 resize() 翻倍搬迁。
第一处为什么:为什么把 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。