简单聊聊 ThreadLocal 源码
这篇基于 jdk1.8.0_231 的
java/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,互不干扰。三个特征:
- 线程隔离------每个线程的副本互不可见;
- 无锁并发------副本各写各的,不存在共享状态;
- 懒初始化 ------首次
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/get时createMap才建(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); }
为什么不用链表?工程权衡,三个理由:
- 存储量小:一个线程的 ThreadLocal 通常就几个,数组比链表省内存、对缓存友好;
- 分布好:魔数保证哈希均匀,碰撞少,开放寻址够用;
- 清理方便 :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/resize →
expungeStaleEntries全表清扫。
源码注释说得很直白:"没有用引用队列,过期条目只在表快满时才保证被清"------所以清理是"尽力而为",不是即时保证。
八、内存泄漏:为什么线程池里必须 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 |
四个坑:
- 线程池必 remove:复用线程导致"上一个任务的值残留" = 脏数据,还可能内存泄漏;
- 别存大对象:值长期挂在长寿线程上,避免把整个对象图塞进 ThreadLocal;
- InheritableThreadLocal 别配线程池 :它只在 Thread 构造时 把父线程的值复制给子线程;线程池是复用线程、不是新建线程,根本不会继承------你会读到上一个任务残留的值,且没人自动清;
- 值要不可变:多线程读同一份初始值时要小心共享可变状态(除非每次 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。