ThreadLocal 使用线性探测法解决哈希冲突,并在 get/set 时顺手清理 key 为 null 的失效条目;初始槽位由位运算哈希确定,冲突时循环探测下一个位置,遇空位或命中 key 停止;探测中遇到 stale entry 即调用 expungeStaleEntry 清理并重哈希后续有效 Entry;该设计兼顾性能与内存安全,但依赖及时调用 remove() 避免探测链过长。

ThreadLocal 的哈希冲突不靠链表或红黑树,而是用线性探测法在数组中“挨个找空位”。它不是被动等冲突再处理,而是在每次 set 或 get 时,只要路过失效条目(key 为 null),就顺手清理——这是它兼顾性能与内存安全的核心设计。
线性探测怎么被触发?
触发条件很明确:计算出的初始槽位 非空,且里面 Entry 的 key 与当前 ThreadLocal 实例不相等。
- 初始索引由
key.threadLocalHashCode & (table.length - 1)得到,是位运算哈希,保证落在合法范围内 - 一旦
tab[i] != null && tab[i].get() != key,立即进入 for 循环探测 - 循环体是
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]),本质就是不断执行i = (i + 1) % len - 停止条件有两个:遇到
e == null(找到空位),或e.get() == key(命中已有条目)
探测过程中如何清理 stale entry?
清理不是额外步骤,而是探测路径上的“顺手操作”:只要当前 Entry 的 key 为 null,就立刻调用 expungeStaleEntry(i)。
- 该方法清空当前槽位,并向后扫描连续非空槽,把所有 key == null 的条目一并清除
- 同时将还能用的 Entry 按新哈希值重新定位,尽量挪到更靠近其理想位置的地方,压缩后续探测距离
- 清理后若发现相同 key 已存在(比如被重哈希挪过来的),直接覆盖 value,不再继续探测
- 注意:只清理当前探测路径上碰到的 stale entry,不会全表扫描,也不绕回数组开头
为什么坚持用线性探测而不是链地址法?
这不是妥协,而是针对 ThreadLocal 使用场景做的精准取舍。
- 单线程内 ThreadLocal 数量通常很少(几十个以内),冲突概率本身低;配合魔数
0x61c88647生成均匀 hash 码,进一步压低冲突 - 数组结构无额外对象开销(不用 Node、next 指针),CPU 缓存局部性好,访问更快
- 清理逻辑能自然嵌入读写路径:探测即清理,避免 stale entry 长期占位导致探测链越来越长
- 没有树化、扩容阈值、负载因子等复杂机制,代码简洁可控,符合“轻量专用 Map”的定位
实际影响和使用提醒
线性探测让 ThreadLocalMap 表现高度依赖使用习惯——写得不对,性能和内存都会出问题。
- 不调用
remove()是最大隐患:key 被 GC 后变成 stale entry,若长期不触发探测,它就卡在数组里,既占位又阻碍查找 - 大量 ThreadLocal 实例未及时清理时,探测路径可能变长,
set/get从 O(1) 退化为 O(n) - 扩容成本高:rehash 需遍历整个 table,且不自动触发,全靠主动 remove 或探测中清理间接推动
- 调试时可在
ThreadLocalMap.set()打断点,观察i(当前索引)、e(当前 Entry)、tab[i]变化,直观看到探测步进和清理时机










