一、前言在上一篇文章中我们明确了 ThreadLocal 的核心是 “数据存在线程的 ThreadLocalMap 中”但 ThreadLocalMap 本身又是如何设计的它和我们常用的 HashMap 有什么区别为什么要用这种设计本文将深入 ThreadLocalMap 的源码拆解它的底层结构、哈希冲突解决方式和核心方法逻辑带你吃透 ThreadLocal 体系的核心细节。二、核心差异在分析 ThreadLocalMap 之前我们先明确它和 HashMap 的核心区别 —— 这是理解其设计思想的关键特性HashMapThreadLocalMap存储结构数组 链表JDK8 新增红黑树纯数组哈希冲突解决方式链地址法冲突元素挂载到链表 / 红黑树线性探测法冲突后向后查找空槽位Key 的特性支持 null 键Key 只能是 ThreadLocal 实例且为弱引用扩容机制扩容为原容量 2 倍重新哈希扩容为原容量 2 倍重新哈希核心设计目标通用键值对存储专为 ThreadLocal 优化轻量高效核心设计考量 ThreadLocalMap 不需要像 HashMap 那样支持通用的键值对存储它的 Key 固定为 ThreadLocal 实例且访问频率高、数据量小因此采用 “数组 线性探测法” 的极简设计以牺牲少量冲突处理效率为代价换取更轻量的内存占用和更快的基础访问速度。三、核心结构1. 底层存储Entry 数组ThreadLocalMap 的底层是一个名为 table 的 Entry 类型数组源码定义如下JDK 8static class ThreadLocalMap { // Entry 是 ThreadLocalMap 的核心存储单元 static class Entry extends WeakReferenceThreadLocal? { // 存储的实际数据ThreadLocal 的值 Object value; // 构造方法Key 是 ThreadLocal 实例且被包装为弱引用 Entry(ThreadLocal? k, Object v) { super(k); value v; } } // 初始容量必须是 2 的幂 private static final int INITIAL_CAPACITY 16; // 存储 Entry 的数组 private Entry[] table; // 数组中已使用的 Entry 数量 private int size 0; // 扩容阈值默认是容量的 2/3 private int threshold; // 设置扩容阈值为容量的 2/3 private void setThreshold(int len) { threshold len * 2 / 3; } }关键细节解析 Entry 继承 WeakReferenceEntry 的 KeyThreadLocal 实例是 弱引用 这是为了避免 ThreadLocal 实例被回收后Key 仍然强引用导致内存泄漏后续内存泄漏文章会详细讲解。初始容量与扩容阈值初始容量为 162 的幂保证哈希分布均匀扩容阈值为当前容量的 2/3当数组中 Entry 数量超过阈值时触发扩容。Value 是强引用Entry 的 Value数据副本是强引用这也是后续可能引发内存泄漏的核心点。2. 哈希值计算ThreadLocal 的 threadLocalHashCodeThreadLocalMap 以 ThreadLocal 实例为 Key它的哈希值并非 Object 的 hashCode() 而是 ThreadLocal 内部维护的 threadLocalHashCode public class ThreadLocalT { // 每次创建 ThreadLocal 实例时自增的哈希种子 private static AtomicInteger nextHashCode new AtomicInteger(); // 哈希值增量黄金分割数保证哈希分布均匀 private static final int HASH_INCREMENT 0x61c88647; // 当前 ThreadLocal 实例的哈希值 private final int threadLocalHashCode nextHashCode.getAndAdd(HASH_INCREMENT); }哈希值计算逻辑 每个 ThreadLocal 实例创建时都会通过 nextHashCode.getAndAdd(HASH_INCREMENT) 获取一个唯一的哈希值。HASH_INCREMENT 是一个黄金分割数0x61c88647能保证多个 ThreadLocal 实例的哈希值均匀分布在 Entry 数组中减少哈希冲突。3. 索引计算拿到 ThreadLocal 的 threadLocalHashCode 后ThreadLocalMap 通过以下公式计算 Entry 在数组中的索引// len 是 Entry 数组的长度2 的幂 int i key.threadLocalHashCode (len - 1);这等价于 key.threadLocalHashCode % len 但位运算的效率更高这也是数组容量必须是 2 的幂的原因。四、核心方法源码解析1. set () 方法核心set() 方法是 ThreadLocalMap 存储数据的核心源码如下关键逻辑已加注释private void set(ThreadLocal? key, Object value) { Entry[] tab table; int len tab.length; // 1. 计算当前 ThreadLocal 对应的数组索引 int i key.threadLocalHashCode (len - 1); // 2. 线性探测法查找空槽位解决哈希冲突 for (Entry e tab[i]; e ! null; e tab[i nextIndex(i, len)]) { ThreadLocal? k e.get(); // 2.1 如果找到相同的 ThreadLocal Key更新 Value if (k key) { e.value value; return; } // 2.2 如果 Key 为 nullThreadLocal 已被回收替换过期 Entry if (k null) { replaceStaleEntry(key, value, i); return; } } // 3. 找到空槽位创建新 Entry 存入 tab[i] new Entry(key, value); int sz size; // 4. 清理过期 Entry若清理后数量仍超过阈值触发扩容 if (!cleanSomeSlots(i, sz) sz threshold) rehash(); } // 获取下一个索引线性探测向后移动一位到末尾则回到开头 private static int nextIndex(int i, int len) { return ((i 1 len) ? i 1 : 0); }set () 方法核心逻辑拆解 哈希计算通过 threadLocalHashCode (len - 1) 计算初始索引。线性探测1如果当前索引的 Entry 不为 null先判断 Key 是否匹配匹配则更新 Value直接返回。2如果 Key 为 null说明 ThreadLocal 实例已被回收调用 replaceStaleEntry() 清理过期 Entry 并存入新值。3如果以上都不满足通过 nextIndex() 向后查找空槽位。如果当前索引的 Entry 不为 null先判断 Key 是否匹配匹配则更新 Value直接返回。如果 Key 为 null说明 ThreadLocal 实例已被回收调用 replaceStaleEntry() 清理过期 Entry 并存入新值。如果以上都不满足通过 nextIndex() 向后查找空槽位。存入新值找到空槽位后创建新 Entry 存入数组。清理与扩容调用 cleanSomeSlots() 清理过期 Entry若数组使用量超过阈值调用 rehash() 扩容。2. getEntry () 方法getEntry() 方法用于根据 ThreadLocal Key 查找对应的 Value源码如下private Entry getEntry(ThreadLocal? key) { // 1. 计算初始索引 int i key.threadLocalHashCode (table.length - 1); Entry e table[i]; // 2. 如果找到匹配的 Key直接返回 Entry if (e ! null e.get() key) return e; else // 3. 未找到则通过线性探测继续查找并清理过期 Entry return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal? key, int i, Entry e) { Entry[] tab table; int len tab.length; while (e ! null) { ThreadLocal? k e.get(); // 找到匹配的 Key返回 Entry if (k key) return e; // 清理过期 Entry if (k null) expungeStaleEntry(i); else // 线性探测向后查找 i nextIndex(i, len); e tab[i]; } // 未找到返回 null return null; }getEntry () 方法核心逻辑 先通过哈希计算初始索引直接查找对应 Entry。如果 Entry 存在且 Key 匹配直接返回。如果不匹配调用 getEntryAfterMiss() 进行线性探测查找同时清理过程中遇到的过期 EntryKey 为 null 的 Entry。若最终未找到返回 null。3. 过期 Entry 清理expungeStaleEntry ()expungeStaleEntry() 是 ThreadLocalMap 的核心清理方法用于移除 Key 为 null 的过期 Entry并重新哈希后续的 Entry源码核心逻辑如下private int expungeStaleEntry(int staleSlot) { Entry[] tab table; int len tab.length; // 1. 清除当前过期 Entry tab[staleSlot].value null; tab[staleSlot] null; size--; // 2. 线性探测后续 Entry重新哈希并清理过期 Entry Entry e; int i; for (i nextIndex(staleSlot, len); (e tab[i]) ! null; i nextIndex(i, len)) { ThreadLocal? k e.get(); // 清理过期 Entry if (k null) { e.value null; tab[i] null; size--; } else { // 重新计算索引解决哈希冲突导致的位置偏移 int h k.threadLocalHashCode (len - 1); if (h ! i) { tab[i] null; // 线性探测找到新的空槽位 while (tab[h] ! null) h nextIndex(h, len); tab[h] e; } } } return i; }核心作用 清理 Key 为 null 的过期 Entry释放 Value 引用避免内存泄漏。对后续的 Entry 重新计算索引并移动位置修复线性探测导致的哈希分布偏移问题。五、扩容机制当 ThreadLocalMap 中 Entry 数量超过阈值容量的 2/3且清理过期 Entry 后仍未缓解会触发扩容private void rehash() { // 先清理所有过期 Entry expungeStaleEntries(); // 若清理后数量仍超过阈值的 3/4触发扩容 if (size threshold - threshold / 4) resize(); } private void resize() { Entry[] oldTab table; int oldLen oldTab.length; // 新容量为原容量的 2 倍 int newLen oldLen * 2; Entry[] newTab new Entry[newLen]; int count 0; // 遍历旧数组重新哈希并放入新数组 for (Entry e : oldTab) { if (e ! null) { ThreadLocal? k e.get(); // 清理过期 Entry if (k null) { e.value null; // 释放 Value 引用 } else { int h k.threadLocalHashCode (newLen - 1); // 线性探测找到空槽位 while (newTab[h] ! null) h nextIndex(h, newLen); newTab[h] e; count; } } } // 设置新的扩容阈值 setThreshold(newLen); size count; table newTab; }扩容核心逻辑 扩容前先调用 expungeStaleEntries() 清理所有过期 Entry尽可能减少数据量。新数组容量为原容量的 2 倍遍历旧数组将有效 Entry 重新哈希后放入新数组。过程中再次清理过期 Entry释放 Value 引用避免内存泄漏。六、总结本文深入剖析了 ThreadLocalMap 的底层结构和核心方法核心要点如下结构设计ThreadLocalMap 底层是 Entry 数组Entry 的 Key 是 ThreadLocal 弱引用Value 是数据副本强引用。冲突解决采用线性探测法解决哈希冲突而非 HashMap 的链地址法适配 ThreadLocal 的轻量使用场景。核心方法set() 方法通过线性探测存储数据并清理过期 Entry getEntry() 方法查找数据时也会清理过期 Entry expungeStaleEntry() 是核心清理方法避免内存泄漏。扩容机制容量满 2/3 触发扩容扩容前先清理过期 Entry新容量为原容量 2 倍重新哈希所有有效 Entry。理解 ThreadLocalMap 的设计是搞懂 ThreadLocal 内存泄漏问题的关键。下一篇文章我们将聚焦 ThreadLocal 最容易踩坑的点 —— 内存泄漏分析其根本原因和解决方案。