一、前言
在上一篇文章中,我们明确了 ThreadLocal 的核心是 “数据存在线程的 ThreadLocalMap 中”,但 ThreadLocalMap 本身又是如何设计的?它和我们常用的 HashMap 有什么区别?为什么要用这种设计?本文将深入 ThreadLocalMap 的源码,拆解它的底层结构、哈希冲突解决方式和核心方法逻辑,带你吃透 ThreadLocal 体系的核心细节。
二、核心差异
在分析 ThreadLocalMap 之前,我们先明确它和 HashMap 的核心区别 —— 这是理解其设计思想的关键:
特性 | HashMap | ThreadLocalMap |
|---|---|---|
存储结构 | 数组 + 链表(JDK8 新增红黑树) | 纯数组 |
哈希冲突解决方式 | 链地址法(冲突元素挂载到链表 / 红黑树) | 线性探测法(冲突后向后查找空槽位) |
Key 的特性 | 支持 null 键 | Key 只能是 ThreadLocal 实例,且为弱引用 |
扩容机制 | 扩容为原容量 2 倍,重新哈希 | 扩容为原容量 2 倍,重新哈希 |
核心设计目标 | 通用键值对存储 | 专为 ThreadLocal 优化,轻量高效 |
核心设计考量 :ThreadLocalMap 不需要像 HashMap 那样支持通用的键值对存储,它的 Key 固定为 ThreadLocal 实例,且访问频率高、数据量小,因此采用 “数组 + 线性探测法” 的极简设计,以牺牲少量冲突处理效率为代价,换取更轻量的内存占用和更快的基础访问速度。
三、核心结构
1. 底层存储:Entry 数组
ThreadLocalMap 的底层是一个名为 table 的 Entry 类型数组,源码定义如下(JDK 8):
static class ThreadLocalMap { // Entry 是 ThreadLocalMap 的核心存储单元 static class Entry extends WeakReference<ThreadLocal<?>> { // 存储的实际数据(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 继承 WeakReference:Entry 的 Key(ThreadLocal 实例)是 弱引用 ,这是为了避免 ThreadLocal 实例被回收后,Key 仍然强引用导致内存泄漏(后续内存泄漏文章会详细讲解)。
初始容量与扩容阈值:初始容量为 16(2 的幂,保证哈希分布均匀),扩容阈值为当前容量的 2/3,当数组中 Entry 数量超过阈值时,触发扩容。
Value 是强引用:Entry 的 Value(数据副本)是强引用,这也是后续可能引发内存泄漏的核心点。
2. 哈希值计算:ThreadLocal 的 threadLocalHashCode
ThreadLocalMap 以 ThreadLocal 实例为 Key,它的哈希值并非 Object 的 hashCode() ,而是 ThreadLocal 内部维护的 threadLocalHashCode :
public class ThreadLocal<T> { // 每次创建 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 为 null(ThreadLocal 已被回收),替换过期 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() 进行线性探测查找,同时清理过程中遇到的过期 Entry(Key 为 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 最容易踩坑的点 —— 内存泄漏,分析其根本原因和解决方案。