线性探测在threadlocalmap.set()中仅当计算出的槽位非空且key不匹配时触发,通过for循环调用nextindex(i, len)(即(i+1)%len)逐个探测后续位置,直至遇到null槽位或匹配key;探测中同步清理stale entry并可能触发rehash。

ThreadLocalMap.set() 中的线性探测是怎么触发的
当调用 ThreadLocal.set() 时,最终会走到 ThreadLocalMap.set(ThreadLocal> key, Object value)。线性探测不是默认行为,它只在「计算出的槽位已被占用,且 key 不匹配」时才启动。
关键判断逻辑是:for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) —— 这个 for 循环就是线性探测的本体。每次循环都把索引 i 推进到下一个位置(nextIndex(i, len)),直到遇到空槽(e == null)或找到匹配的 key。
- 初始索引
i = key.threadLocalHashCode & (len - 1),这是位运算哈希,保证落在[0, len-1]范围内 - 只要
tab[i]非空,就进入循环;第一次不探测,从第二次开始才是“探测” -
nextIndex(i, len)实际就是(i + 1) % len,即简单加一、到尾回绕
探测过程中遇到 key == null 的 Entry 怎么办
线性探测不只是找空位,还会顺手清理「过时条目(stale entry)」—— 即 Entry 的 get() 返回 null(因为 ThreadLocal 被 GC 回收,弱引用失效),但 value 还强引用着。
此时不会跳过,而是调用 expungeStaleEntry(i):清空该槽位,并把后续连续非空槽中 key 为 null 的 Entry 全部清理,同时把还能用的 Entry 按新哈希值「重哈希」挪到更靠近其理想位置的地方。
统一邮件管理技能,用于读取、发送、回复、转发邮件及管理草稿箱和文件夹。用户提及「读邮件」「发邮件」「回复」「草稿箱」「未读邮件」「收件箱」「邮箱」等邮件相关操作时使用此技能。基于 Python 脚本操作任意 IMAP/SMTP 邮箱。
- 这步清理不是可选优化,是防止内存泄漏的关键动作
- 清理后,当前待插入的
key-value会尝试插到刚腾出的位置,而不是继续往后找 - 如果清理后发现已有相同
key的 Entry(比如被重哈希挪过来的),就直接覆盖value
为什么不用链地址法而坚持线性探测
ThreadLocalMap 不是通用 Map,它的设计目标非常明确:每个线程最多存几十个 ThreadLocal,且读多写少、生命周期与线程绑定。线性探测在这种场景下比链地址法更轻量:
- 没有额外对象开销(不需要 Node 节点、next 指针)
- 数组局部性好,CPU 缓存友好;探测路径短,平均查找成本低
- 避免了 HashMap 中树化/退化等复杂逻辑,代码更可控
- 配合弱引用 key 和自动 stale 清理,能较好平衡内存安全与性能
但代价是:扩容成本高(必须 rehash 整个 table),且对哈希分布更敏感——所以用了魔数 0x61c88647 来生成均匀的 threadLocalHashCode。
实际调试时怎么观察线性探测过程
可以在 ThreadLocalMap.set() 方法里加断点,重点关注三处变量变化:i(当前索引)、e(当前 Entry)、tab[i](对应槽位内容)。手动构造哈希冲突最简单的方式是复用同一个 ThreadLocal 实例多次 set(不会触发探测),或者用反射修改不同 ThreadLocal 的 threadLocalHashCode,强制它们落在同一槽位。
- 注意:JDK 9+ 后
threadLocalHashCode是final,反射需用Unsafe或降级到 JDK 8 测试 - 探测步数超过阈值(
len / 2)通常意味着 table 太小或哈希严重不均,这时会先触发resize() - 不要依赖探测步数做性能假设——真实业务中绝大多数 set 操作都在第一步命中空槽或匹配 key
线性探测本身不难理解,难的是它和弱引用、stale 清理、rehash 扩容这几件事缠在一起执行;漏掉任意一环,都可能让内存泄漏在压测后期突然爆发。










