ThreadLocal 实现原理
API 用起来简单,内部结构却不简单。
理解 ThreadLocal 的实现原理,有助于你在出问题时快速定位,也能在阅读 Spring、MyBatis 等框架源码时不至于两眼一抹黑。
核心组件关系
┌──────────────────────────────────────────────────────────┐
│ ThreadLocal 实现结构 │
├──────────────────────────────────────────────────────────┤
│ │
│ ThreadLocal<T> │
│ ┌────────────────────────────────────────────────┐ │
│ │ public T get() │ │
│ │ public void set(T value) │ │
│ │ public void remove() │ │
│ │ protected T initialValue() │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ ThreadLocal.ThreadLocalMap(内部类) │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ Entry[] table │ │ │
│ │ │ ┌─────────┬─────────┬─────────┐ │ │ │
│ │ │ │ Entry │ Entry │ Entry │ ... │ │ │
│ │ │ │ key* │ key* │ key* │ │ │ │
│ │ │ │ value │ value │ value │ │ │ │
│ │ │ └─────────┴─────────┴─────────┘ │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Thread │
│ ┌────────────────────────────────────────────────┐ │
│ │ ThreadLocal.ThreadLocalMap threadLocals │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ 存储在 Thread 对象内部,ThreadLocal 只是访问入口 │
│ │
└──────────────────────────────────────────────────────────┘三个关键点:
ThreadLocal是访问入口,本身不存储数据- 数据存储在
Thread.threadLocals中(ThreadLocalMap 类型) - 每个 Thread 都有自己的 ThreadLocalMap,天然隔离
ThreadLocalMap 结构
java
// ThreadLocalMap 是 ThreadLocal 的静态内部类
static class ThreadLocalMap {
// Entry 继承 WeakReference,key 为 ThreadLocal
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 弱引用
value = v; // 强引用
}
}
private Entry[] table; // Entry 数组,核心存储结构
private int size = 0; // 实际元素数量
private int threshold; // 扩容阈值(默认 length * 2/3)
}get() 源码解析
java
public T get() {
// 1. 获取当前线程
Thread t = Thread.currentThread();
// 2. 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t); // 等价于 t.threadLocals
// 3. map 存在,查找 Entry
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 4. map 不存在 或 找不到 Entry → 设置初始值
return setInitialValue();
}
private T setInitialValue() {
// 调用 initialValue() 获取初始值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value); // 存入 map
} else {
createMap(t, value); // 首次访问时创建 map
}
return value;
}流程图:
get()
│
├─► Thread.currentThread()
│
├─► getMap(t) → t.threadLocals
│
├─► map.getEntry(this) 查找
│ │
│ ├─► 找到 → 返回 value
│ └─► 找不到 → setInitialValue()
│
└─► setInitialValue()
├─► 调用 initialValue()
├─► 创建 ThreadLocalMap(如需要)
└─► 存入初始值set() 源码解析
java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// map 已存在,直接存入
map.set(this, value);
} else {
// 首次 set,创建 map
createMap(t, value);
}
}
// createMap 等价于:
// t.threadLocals = new ThreadLocalMap(this, firstValue);流程图:
set(value)
│
├─► Thread.currentThread()
│
├─► getMap(t)
│
├─► map != null → map.set(this, value)
│ │
│ └─► 线性探测法处理哈希冲突
│
└─► map == null → createMap(t, value)
│
└─► t.threadLocals = new ThreadLocalMap(this, value)remove() 源码解析
java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this); // 删除当前 ThreadLocal 对应的 Entry
}
}
// ThreadLocalMap.remove(key) 内部:
// 1. 通过哈希找到位置
// 2. 调用 e.clear() 清除弱引用(key 置 null)
// 3. 调用 expungeStaleEntry() 清理 value 并重新哈希expungeStaleEntry() 的工作:
expungeStaleEntry(staleSlot)
│
├─► table[staleSlot].value = null ← 释放 value
├─► table[staleSlot] = null ← 释放 Entry
├─► 遍历后续 Entry,重新哈希(填补空位)
└─► size--哈希算法:黄金分割哈希
ThreadLocal 使用了一个精心设计的哈希算法,避免哈希冲突:
java
// ThreadLocal 的哈希码生成
private final int threadLocalHashCode = nextHashCode();
// 每次创建新的 ThreadLocal 都递增
private static AtomicInteger nextHashCode =
new AtomicInteger();
// 固定增量:0x61c88647(黄金分割数)
private static final int HASH_INCREMENT = 0x61c88647;为什么选 0x61c88647?因为它的二进制是 32 位的黄金分割比例,用它递增生成的哈希码在数组中分布均匀,减少冲突。
tl1 → hash: 0x61c88647
tl2 → hash: 0xc3910c8e
tl3 → hash: 0x12595931
...哈希冲突解决:线性探测
ThreadLocalMap 使用开放地址法(线性探测)解决哈希冲突:
假设 table length = 8,插入三个 ThreadLocal:
index = hash & (len - 1)
tl1: index = 0 → table[0]
tl2: index = 3 → table[3]
tl3: index = 0(冲突)→ table[1](线性探测到第一个空位)查找时也是线性探测:
getEntry(key)
│
├─► 计算 index = key.threadLocalHashCode & (len-1)
│
├─► table[i] == key → 找到
│
├─► table[i] == null → 不存在
│
└─► 冲突 → i++ 继续探测扩容机制
当 table 中的 Entry 数量超过阈值的 2/3 时,触发 rehash 扩容:
java
private void rehash() {
// 先全量清理 stale Entry
expungeStaleEntries();
// 如果清理后 size >= threshold - threshold/4,扩容
if (size >= threshold - threshold / 4)
resize();
}
private void resize() {
// 容量翻倍
Entry[] newTable = new Entry[oldCapacity * 2];
// 重新哈希所有 Entry
for (Entry e : oldTable) {
if (e != null) {
// ...
newTable[newIndex] = e;
}
}
}内存泄漏防护机制回顾
ThreadLocalMap 的防护设计:
| 机制 | 说明 |
|---|---|
| Entry 用弱引用存 key | ThreadLocal 置 null 后,key 可被 GC |
| expungeStaleEntry() | 清理 key==null 的 Entry,释放 value |
| 触发时机 | get() / set() / remove() 时延迟清理 |
| 不等于安全 | 仍需手动 remove() |
一句话总结原理
ThreadLocal 本身不存值,它只是给 Thread 对象打了个「洞」,让每个线程都能往自己的口袋里存取数据。
- 入口在
ThreadLocal,存储在Thread.threadLocals - 每个线程都有自己的 Map,天然线程隔离
- 使用黄金分割哈希 + 线性探测解决冲突
- Entry 用弱引用存 key,但 value 仍需手动清理
相关链接
- ThreadLocal 核心概念 - 基本用法
- ThreadLocal 方法详解 - API 详解
- ThreadLocal 内存泄漏 - 泄漏原因
