ThreadLocalMap 结构解析与核心方法源码
2026/7/17 11:41:11 网站建设 项目流程

一、前言

在上一篇文章中,我们明确了 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 () 方法核心逻辑拆解 :

  1. 哈希计算:通过 threadLocalHashCode & (len - 1) 计算初始索引。

  2. 线性探测:

    1)如果当前索引的 Entry 不为 null,先判断 Key 是否匹配:匹配则更新 Value,直接返回。

    2)如果 Key 为 null(说明 ThreadLocal 实例已被回收),调用 replaceStaleEntry() 清理过期 Entry 并存入新值。

    3)如果以上都不满足,通过 nextIndex() 向后查找空槽位。

  3. 如果当前索引的 Entry 不为 null,先判断 Key 是否匹配:匹配则更新 Value,直接返回。

  4. 如果 Key 为 null(说明 ThreadLocal 实例已被回收),调用 replaceStaleEntry() 清理过期 Entry 并存入新值。

  5. 如果以上都不满足,通过 nextIndex() 向后查找空槽位。

  6. 存入新值:找到空槽位后,创建新 Entry 存入数组。

  7. 清理与扩容:调用 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 () 方法核心逻辑 :

  1. 先通过哈希计算初始索引,直接查找对应 Entry。

  2. 如果 Entry 存在且 Key 匹配,直接返回。

  3. 如果不匹配,调用 getEntryAfterMiss() 进行线性探测查找,同时清理过程中遇到的过期 Entry(Key 为 null 的 Entry)。

  4. 若最终未找到,返回 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; }

扩容核心逻辑 :

  1. 扩容前先调用 expungeStaleEntries() 清理所有过期 Entry,尽可能减少数据量。

  2. 新数组容量为原容量的 2 倍,遍历旧数组,将有效 Entry 重新哈希后放入新数组。

  3. 过程中再次清理过期 Entry,释放 Value 引用,避免内存泄漏。

六、总结

本文深入剖析了 ThreadLocalMap 的底层结构和核心方法,核心要点如下:

  1. 结构设计:ThreadLocalMap 底层是 Entry 数组,Entry 的 Key 是 ThreadLocal 弱引用,Value 是数据副本强引用。

  2. 冲突解决:采用线性探测法解决哈希冲突,而非 HashMap 的链地址法,适配 ThreadLocal 的轻量使用场景。

  3. 核心方法:set() 方法通过线性探测存储数据并清理过期 Entry; getEntry() 方法查找数据时也会清理过期 Entry; expungeStaleEntry() 是核心清理方法,避免内存泄漏。

  4. 扩容机制:容量满 2/3 触发扩容,扩容前先清理过期 Entry,新容量为原容量 2 倍,重新哈希所有有效 Entry。

理解 ThreadLocalMap 的设计,是搞懂 ThreadLocal 内存泄漏问题的关键。下一篇文章,我们将聚焦 ThreadLocal 最容易踩坑的点 —— 内存泄漏,分析其根本原因和解决方案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询