欢迎光临
我们一直在努力

ThreadLocalMap 结构解析与核心方法源码

一、前言

在上一篇文章中,我们明确了 ThreadLocal 的核心是 “数据存在线程的 ThreadLocalMap 中”,但 ThreadLocalMap 本身又是如何设计的?它和我们常用的 HashMap 有什么区别?为什么要用这种设计?本文将深入 ThreadLocalMap 的源码,拆解它的底层结构、哈希冲突解决方式和核心方法逻辑,带你吃透 ThreadLocal 体系的核心细节。

二、核心差异

在分析 ThreadLocalMap 之前,我们先明确它和 HashMap 的核心区别 —— 这是理解其设计思想的关键:

特性

HashMap

ThreadLocalMap

存储结构

哈希冲突解决方式

Key 的特性

扩容机制

核心设计目标

数组 + 链表(JDK8 新增红黑树)

纯数组

链地址法(冲突元素挂载到链表 / 红黑树)

线性探测法(冲突后向后查找空槽位)

支持 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 最容易踩坑的点 —— 内存泄漏,分析其根本原因和解决方案。

    赞(0)
    未经允许不得转载:171主机测评 » ThreadLocalMap 结构解析与核心方法源码
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址