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() 方法,才能在高并发场景下安全高效地管理线程局部变量。