ThreadLocalMap 是 ThreadLocal
的核心底层数据结构,负责在每个Thread线程中存储与 ThreadLocal
实例绑定的数据。它的设计目标是高效管理线程隔离数据,同时尽量减少内存泄漏风险。以下是其核心实现细节。
数据结构与设计目标
核心结构
Entry 数组:
ThreadLocalMap
内部维护一个Entry[]
数组,每个Entry
表示一个键值对。static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // 值(强引用) Entry(ThreadLocal<?> k, Object v) { super(k); // Key 是弱引用(继承自 WeakReference) value = v; } }
Key:
ThreadLocal
实例(弱引用,避免内存泄漏)。Value:线程绑定的数据(强引用,需手动或惰性清理)。
哈希算法:
通过ThreadLocal.threadLocalHashCode
计算索引位置(类似取模运算):int i = key.threadLocalHashCode & (table.length - 1); // table.length 是 2 的幂
设计目标
线程隔离:每个线程独立维护自己的
ThreadLocalMap
。内存高效:通过弱引用和惰性清理减少内存泄漏。
低冲突率:使用线性探测法(开放寻址)处理哈希冲突。
哈希冲突解决:开放寻址法(线性探测法)
与 HashMap
的链表法不同,ThreadLocalMap
使用 开放寻址法(线性探测法) 解决冲突:
插入流程:
计算初始索引
i = hash & (len-1)
。若
table[i]
已被占用(Key 不同),则向后遍历(i = nextIndex(i, len)
)直到找到空槽。
查找流程:
计算初始索引
i
,若table[i]
的 Key 不匹配,则向后遍历直到找到目标或空槽。
删除流程:
清理当前槽位,并触发探测式清理(
expungeStaleEntry
),避免后续查找因空槽中断。
示例:插入一个 Key 到冲突位置
// 假设 table[3] 已被占用,Key 不同
hash = key.threadLocalHashCode;
i = hash & (len-1); // 初始计算为 3
while (table[i] != null) {
i = nextIndex(i, len); // 线性探测,i=4,5,6...
}
table[i] = new Entry(key, value);
内存管理机制
惰性清理(Lazy Cleanup)
在以下操作中触发清理无效 Entry(Key 为 null
的 Entry):
getEntry(ThreadLocal<?> key)
:查找时发现 Key 已被回收,触发清理。set(ThreadLocal<?> key, Object value)
:插入时遇到无效 Entry,触发清理。remove(ThreadLocal<?> key)
:直接清理指定 Entry。
核心方法 expungeStaleEntry(int staleSlot)
:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 1. 清理当前槽位
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 2. 向后探测清理连续段中的无效 Entry
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) { // 该 Entry 本应位于其他位置(因冲突被挤到此处)
tab[i] = null; // 清空当前槽位
// 重新哈希到正确位置
while (tab[h] != null) h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
扩容机制
扩容发生在向
ThreadLocalMap
添加 (set
) 新条目或替换现有条目时,当满足特定条件就会触发。整个过程可以分解为以下几个关键步骤:初始化和容量:
初始时,
ThreadLocalMap
是null
。当第一次调用
threadLocal.set(value)
时,会为当前线程创建ThreadLocalMap
。初始容量 (
INITIAL_CAPACITY
): 默认是 16 (一个Entry
数组,长度 16)。阈值 (
threshold
): 触发扩容的临界点。初始阈值是len * 2 / 3
。对于初始容量 16,阈值就是16 * 2 / 3 ≈ 10
(JDK 源码中实际计算是len * 2 / 3
,然后向下取整或类似处理,确保是整数)。
触发扩容的条件 (
set
方法内):
在尝试将新的Entry
放入Entry
数组时:步骤 1: 清理过期 Entry (探测式清理):详见惰性清理(Lazy Cleanup)
步骤 2: 检查是否达到扩容阈值
在清理过期
Entry
之后,如果数组中非null
的Entry
总数(包括有效的和未清理到的过期Entry
)>=
当前的threshold
,则触发扩容检查。
步骤 3: 二次清理 (启发式扫描清理)
在触发扩容检查后,不会立即扩容!而是先进行一次
cleanSomeSlots
(启发式扫描清理)。这个清理会从当前位置开始,对数级别地扫描 (log2(n)
) 一定数量的槽位,尝试清理遇到的过期Entry
。目的: 尽量在扩容前清理掉一些过期
Entry
,避免不必要的扩容开销(分配新数组、复制数据)。
步骤 4: 最终扩容判定
如果
cleanSomeSlots
清理后,数组中非null
的Entry
总数 仍然>=
threshold
的3/4
(注意:这个3/4
是 JDK 源码中一个硬编码的判断点,不是threshold
本身),则真正触发扩容resize()
。关键点: 扩容的条件非常严格。它要求即使在尽力清理过期
Entry
之后,有效Entry
(加上残留的过期Entry
)的数量仍然非常高(超过threshold * 3/4
)。这体现了ThreadLocalMap
对内存占用比较敏感,尽量避免不必要的扩容。
扩容过程 (
resize()
方法):
一旦确定需要扩容:创建新数组: 创建一个新的
Entry
数组,其长度是 旧数组长度的 2 倍。新容量也必须是 2 的幂(初始 16 -> 32 -> 64 ...)。重新计算阈值: 新数组的阈值
newThreshold
=newLen * 2 / 3
。遍历旧数组 & 重新哈希:
遍历旧数组中的每一个
Entry
(e
)。如果
e != null
且e.get() != null
(即Key
还没有被 GC 回收,是有效的Entry
):计算该
Entry
在新数组中的位置i
(根据e.get()
即ThreadLocal
的哈希码和新数组长度计算索引)。线性探测新位置: 如果新位置
i
上已经有Entry
(发生冲突),则线性向后探测(i = nextIndex(i, newLen)
)直到找到一个null
的空槽。插入: 将有效的旧
Entry
放入新数组找到的空槽中。
如果
e != null
但e.get() == null
(即Key
已被回收,过期Entry
),则跳过它!这些过期Entry
在这次扩容过程中被丢弃(清理掉)。
更新引用: 将
ThreadLocalMap
的table
引用指向新创建的数组。更新阈值: 将
threshold
更新为newThreshold
。
与 HashMap 的对比
典型应用场景
线程隔离数据存储:
每个线程的数据库连接、事务上下文等。
性能优化:
避免线程间竞争(如 SimpleDateFormat 的线程安全封装)。
框架级使用:
Spring 的
RequestContextHolder
、TransactionSynchronizationManager
。
注意事项与最佳实践
避免内存泄漏:
使用完
ThreadLocal
后必须调用remove()
,尤其是线程池环境。避免使用静态
ThreadLocal
(静态变量强引用 Key,导致弱引用失效)。
减少哈希冲突:
控制每个线程的
ThreadLocal
实例数量。
谨慎扩容:
频繁扩容会影响性能,初始化时预估合理容量。
线程池上下文污染
线程池中的线程会被多个任务复用。如果前一个任务设置了
ThreadLocal
的值但没有清理,后一个任务直接复用这个线程时,就能读到前一个任务设置的“脏数据”,导致难以预料的错误。这要求开发者必须非常小心地在每次任务执行后精确地清理(remove()
)属于该任务的ThreadLocal
数据。
总结
ThreadLocalMap
是 ThreadLocal
实现线程隔离存储的核心,其通过 弱引用 Key、开放寻址法(线性探测法) 和 惰性清理机制,在保证高效访问的同时降低内存泄漏风险。开发者需理解其底层逻辑,正确使用 remove()
方法,才能在高并发场景下安全高效地管理线程局部变量。