ThreadLocal 详解
2026/2/18约 6018 字
ThreadLocal 介绍
此介绍部分和快速入门存在重叠部分,可以跳过。
什么是 ThreadLocal?
根据Java官方文档的描述,ThreadLocal 类用于提供线程内部的局部变量。这些变量在多线程环境下访问(通过get和set方法)时,能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal 实例通常被声明为 private static 类型,用于关联线程和线程上下文。
核心特性:
- 线程安全:在多线程并发的场景下保证数据访问的线程安全。
- 数据传递:在同一线程中,可以在不同组件间通过
ThreadLocal传递公共变量,避免显式传参。 - 线程隔离:每个线程的变量都是独立的,互不干扰。
基本使用
常用方法:
| 方法声明 | 描述 |
|---|---|
ThreadLocal() | 创建一个 ThreadLocal 对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
使用案例对比:
- 不使用 ThreadLocal(数据混乱):多个线程访问同一个实例变量,数据相互覆盖。
- 使用 ThreadLocal(线程隔离):每个线程都有自己的变量副本,数据互不干扰。
输出结果清晰地展示了 ThreadLocal 如何解决多线程数据隔离的问题。
ThreadLocal 与 Synchronized 的区别
两者都用于处理多线程并发访问变量的问题,但角度和思路不同:
| 对比项 | Synchronized | ThreadLocal |
|---|---|---|
| 原理 | 以时间换空间。只提供一份变量,让多个线程排队访问。 | 以空间换时间。为每个线程提供一份变量的副本,实现同时访问而互不干扰。 |
| 侧重点 | 多个线程之间访问资源的同步。 | 多线程中让每个线程之间的数据相互隔离。 |
在只需数据隔离的场景下,使用 ThreadLocal 能让程序拥有更高的并发性。
ThreadLocal 的优势与应用
两个突出优势:
- 传递数据:保存每个线程绑定的数据,在需要的地方直接获取,避免参数传递带来的代码耦合。
- 线程隔离:各线程数据相互隔离,避免同步方式带来的性能损失。
经典应用:Spring 事务管理 Spring 框架借助 ThreadLocal 来管理数据库连接(Connection)。当一个事务需要执行多个数据库操作时,Spring 会从连接池获取一个 Connection,并将其绑定到当前线程的 ThreadLocal 中。这样,事务中的各个 DAO 方法都可以从 ThreadLocal 中获取同一个 Connection,而无需通过参数显式传递,从而实现了事务的统一提交或回滚。
ThreadLocal 源码概述
早期 JDK 版本的设计
每个 ThreadLocal 维护一个 Map,以线程作为 Map 的 key,以变量副本作为 value。
现在的设计(JDK 8,当然一直到 JDK 21 主体逻辑也没啥很大的变化)
优化设计是:每个 Thread 维护一个 ThreadLocalMap。
Thread类内部有一个ThreadLocalMap类型的成员变量threadLocals。ThreadLocalMap的key是ThreadLocal实例本身(弱引用),value是线程的变量副本。ThreadLocal本身不存储值,它只是作为key来让线程从自己的ThreadLocalMap中获取值。
这样设计的好处
- 减少 Entry 数量:
Map的存储数量由ThreadLocal对象的数量决定。在实际应用中,ThreadLocal的数量通常远少于线程的数量。 - 自动内存回收:当线程销毁时,其内部的
ThreadLocalMap也会随之销毁,有助于减少内存使用。
接下来看看 ThreadLocal 源码的具体实现。
整体架构图
Thread 对象 (每个线程)
│
├─ threadLocals (字段) ──────────────> ThreadLocalMap
│ │
│ ├─ table[] (Entry数组)
│ │ │
│ │ ├─ Entry(key=ThreadLocal①, value=值1)
│ │ ├─ Entry(key=ThreadLocal②, value=值2)
│ │ └─ Entry(key=ThreadLocal③, value=值3)
│ │
│ ├─ size
│ └─ threshold
│
└─ inheritableThreadLocals (字段) ────> ThreadLocalMap (父子线程继承用)核心字段源码
// Thread.java (JDK 21)
public class Thread implements Runnable {
// 每个线程持有的 ThreadLocalMap,由 ThreadLocal 类维护
// 这是 ThreadLocal 机制的核心数据结构,存储了该线程所有的 ThreadLocal 变量
// 注意:这是包私有(package-private)字段,没有 getter/setter 方法
// 同一个包下的 ThreadLocal 类可以直接访问这个字段
ThreadLocal.ThreadLocalMap threadLocals;
// 可继承的 ThreadLocalMap,由 InheritableThreadLocal 类维护
// 当创建子线程时,父线程的 inheritableThreadLocals 中的值会被复制到子线程
// 实现了线程局部变量的继承机制
ThreadLocal.ThreadLocalMap inheritableThreadLocals;
// 获取当前线程的载体线程
// 如果当前线程是虚拟线程,这个方法返回执行该虚拟线程的底层平台线程
// @IntrinsicCandidate 注解表示这是一个内部固有方法,JVM 会进行特殊优化
@IntrinsicCandidate
static native Thread currentCarrierThread();
// 获取当前线程
// 无论当前是平台线程还是虚拟线程,都返回对应的 Thread 对象
// 这是获取当前线程的静态方法,被广泛使用
@IntrinsicCandidate
public static native Thread currentThread();
}ThreadLocal 中的访问方式
// ThreadLocal.java
public class ThreadLocal<T> {
// 获取线程的 ThreadLocalMap
// 这个方法直接返回线程的 threadLocals 字段,没有通过任何方法调用
// 这种设计是为了性能考虑,避免额外的方法调用开销
// InheritableThreadLocal 会重写这个方法,返回 inheritableThreadLocals 字段
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 创建线程的 ThreadLocalMap
// 当线程第一次使用 ThreadLocal 时调用,创建 map 并赋值给 threadLocals 字段
// 这是懒加载机制的体现:只有真正需要时才创建 map
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}ThreadLocalMap 内部结构
// ThreadLocal.java
public class ThreadLocal<T> {
// ThreadLocalMap 是 ThreadLocal 的静态内部类
// 静态内部类意味着它可以不依赖于外部 ThreadLocal 实例而存在
// 这是一个专门为 ThreadLocal 设计的哈希表,采用开放地址法解决冲突
static class ThreadLocalMap {
// Entry 继承 WeakReference,键为 ThreadLocal 的弱引用
// 这是防止内存泄漏的关键设计:ThreadLocal 对象不再被使用时可以被 GC 回收
// 当 ThreadLocal 被回收后,entry.get() 返回 null,形成 stale entry
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 实际存储的值,是强引用
Entry(ThreadLocal<?> k, Object v) {
super(k); // ThreadLocal 作为弱引用键
value = v;
}
}
// 哈希表数组,存储所有的 Entry
// 数组长度必须是 2 的幂,方便使用位运算替代取模
private Entry[] table;
// 当前存储的元素个数
private int size = 0;
// 扩容阈值,当 size 达到这个值时触发扩容
private int threshold;
// 默认初始容量,必须是 2 的幂
private static final int INITIAL_CAPACITY = 16;
// 设置阈值,为容量的 2/3
// 之所以是 2/3 而不是 1/2 或 3/4,是因为开放地址法需要预留足够的空槽
// 负载因子过高会导致冲突增加,影响性能
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 获取环形数组的下一个索引
// 如果 i+1 小于 len,返回 i+1;否则返回 0(回到数组开头)
// 这是开放地址法线性探测的基础
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
// 获取环形数组的上一个索引
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
}
}哈希码生成机制
public class ThreadLocal<T> {
// 每个 ThreadLocal 实例唯一的哈希码
// 这是一个 final 字段,一旦生成就不会改变
// 这个哈希码只用于 ThreadLocalMap 内部的哈希计算
private final int threadLocalHashCode = nextHashCode();
// 静态原子计数器,所有 ThreadLocal 实例共享
// 使用 AtomicInteger 保证线程安全,确保每个实例获得的哈希码都不同
private static final AtomicInteger nextHashCode = new AtomicInteger();
// 哈希增量 - 黄金分割数 0x61c88647
// 这个数非常特殊,它是 2^32 乘以黄金分割比 (√5-1)/2 的整数近似
// 用这个数作为步长递增,生成的哈希值在 2 的幂的数组上分布非常均匀
// 可以有效减少哈希冲突
private static final int HASH_INCREMENT = 0x61c88647;
// 生成下一个哈希码
// 每次调用都会在原值基础上增加 HASH_INCREMENT
// 由于是原子操作,即使多线程同时创建 ThreadLocal 也能保证唯一性
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}引用链路分析
/*
强引用链路(阻止 GC 回收):
Thread 对象
↓ (强引用)
threadLocals (ThreadLocalMap)
↓ (强引用)
table[] (Entry数组)
↓ (强引用)
Entry 对象
↓ (强引用)
value (实际数据)
弱引用链路(允许 GC 回收):
Entry (WeakReference)
↓ (弱引用)
ThreadLocal 对象
内存泄漏风险点:
1. 当 ThreadLocal 对象不再被业务代码引用时(只剩 Entry 的弱引用)
→ GC 可以回收 ThreadLocal 对象
→ Entry.get() 返回 null,形成 stale entry
→ 但 Entry.value 仍然是强引用,无法被回收
→ 如果不清理这个 stale entry,value 会一直存在
2. 线程池场景:
→ 线程长期存活,不会结束
→ 如果不主动调用 remove()
→ stale entry 可能一直存在
→ value 永远不会被 GC 回收
→ 导致内存泄漏
解决方案:
1. 自动清理:get/set/remove 操作时会触发 expungeStaleEntry 清理 stale entry
2. 手动清理:在 finally 块中调用 remove() 方法
3. 弱引用设计:至少保证了 ThreadLocal 对象本身可以被回收
*/set 方法详解
set 方法执行流程图
set(value)
│
├─► 获取当前线程
│
├─► 获取线程的 threadLocals 字段
│ ├─► 存在?───是───► map.set(this, value)
│ │ │
│ │ ├─► 计算哈希索引 i = hash & (len-1)
│ │ │
│ │ ├─► 线性探测
│ │ │ ├─► refersTo(key)?───是───► 替换value,返回
│ │ │ ├─► refersTo(null)?───是───► replaceStaleEntry
│ │ │ └─► 继续探测
│ │ │
│ │ └─► 找到空槽,创建新Entry
│ │ │
│ │ └─► 检查是否需要清理/扩容
│ │
│ └─► 不存在?───是───► createMap(thread, value)
│ │
│ └─► 创建 ThreadLocalMap
│ │
│ ├─► 初始化 table 数组
│ ├─► 计算索引,放入 Entry
│ └─► 设置 size 和 threshold
│
├─► 虚拟线程且开启追踪?───是───► dumpStackIfVirtualThread()
│
└─► 结束set 方法入口源码详解
// ThreadLocal.java
public void set(T value) {
// 调用私有方法,传入当前线程
// 这种设计允许子类(如 CarrierThreadLocal)重写行为
// 通过将当前线程作为参数传入,可以针对不同线程类型做特殊处理
set(Thread.currentThread(), value);
// 虚拟线程调试特性(JDK 21 新增)
// 当设置了 -Djdk.traceVirtualThreadLocals=true 系统属性时启用
// 用于帮助开发者识别虚拟线程中不恰当的 ThreadLocal 使用
if (TRACE_VTHREAD_LOCALS) {
dumpStackIfVirtualThread();
}
}
private void set(Thread t, T value) {
// 获取线程的 ThreadLocalMap
// getMap 方法直接返回 t.threadLocals 字段
ThreadLocalMap map = getMap(t);
if (map != null) {
// map 已存在,将当前 ThreadLocal 和 value 存入 map
// 这里传入 this 作为 key,因为当前 ThreadLocal 实例就是键
map.set(this, value);
} else {
// map 不存在,说明这是该线程第一次使用 ThreadLocal
// 创建新的 ThreadLocalMap 并赋值给线程的 threadLocals 字段
createMap(t, value);
}
}ThreadLocalMap 构造方法详解
// 首次 set 时调用此构造方法创建 ThreadLocalMap
// 这是懒加载机制的体现:只有在第一次使用时才创建 map
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化数组,默认容量16
// 数组长度必须是2的幂,这是为了能用位运算替代取模
table = new Entry[INITIAL_CAPACITY];
// 计算第一个 entry 的索引位置
// threadLocalHashCode & (容量-1) 等价于 hash % 容量
// 因为容量是2的幂,所以可以用位运算优化,性能更高
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 创建第一个 Entry 并放入数组
table[i] = new Entry(firstKey, firstValue);
// 设置元素个数
size = 1;
// 设置扩容阈值 = 16 * 2/3 ≈ 10
// 这意味着当元素达到10个时就会触发扩容
setThreshold(INITIAL_CAPACITY);
}核心 set 方法详解
// ThreadLocalMap.set 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算哈希桶位置:hash & (len-1)
// 由于 len 是2的幂,len-1 的二进制全是1,与操作相当于取模
int i = key.threadLocalHashCode & (len - 1);
// 线性探测法遍历
// 从计算出的位置开始,直到找到空槽为止
// 线性探测是开放地址法的一种,冲突时顺序查找下一个位置
for (Entry e = tab[i];
e != null; // 遇到空槽就停止
e = tab[i = nextIndex(i, len)]) {
// refersTo(key) 是 JDK 16+ 新增的方法
// 比 e.get() == key 更高效,因为它避免了完整的引用访问语义
// 检查这个 Entry 的 key 是否就是我们要找的 ThreadLocal
if (e.refersTo(key)) {
// 情况1:找到相同的 key,直接替换值
// 这是最简单的 case,类似于 HashMap 的 put 操作
e.value = value;
return;
}
// refersTo(null) 检查 key 是否已被 GC 回收
// 如果返回 true,说明这个 Entry 的 ThreadLocal 已经被回收了
// 这样的 Entry 被称为 "stale entry"
if (e.refersTo(null)) {
// 情况2:发现 stale entry
// 需要替换这个 stale entry,并清理周围的 stale entries
// 这是 ThreadLocal 清理机制的核心
replaceStaleEntry(key, value, i);
return;
}
// 情况3:冲突且 key 不同,继续下一次探测
}
// 情况4:找到空槽位,创建新 Entry
// 这是最理想的情况,直接插入即可
tab[i] = new Entry(key, value);
int sz = ++size;
// 清理并判断是否需要扩容
// cleanSomeSlots 进行启发式清理,返回 true 表示清理了 stale entry
// 如果没有清理且 size 达到阈值,就执行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // rehash 会先全面清理,然后判断是否扩容
}replaceStaleEntry 方法详解
// 替换 stale entry 并进行清理
// 这个方法处理在 set 操作中遇到 stale entry 的情况
// staleSlot 参数是发现 stale entry 的位置
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 向前扫描,找到当前运行(run)中最前面的 stale entry
// "运行"是指两个 null 槽之间的一段连续非空 Entry 序列
// 我们一次清理整个运行,避免因为 GC 批量释放引用而导致的持续增量重哈希
int slotToExpunge = staleSlot; // 需要清理的起始位置
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)) {
if (e.refersTo(null)) {
slotToExpunge = i; // 更新清理起点为更靠前的位置
}
}
// 向后扫描,查找 key 或者继续找 stale entry
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 如果找到相同的 key
if (e.refersTo(key)) {
e.value = value; // 替换值
// 交换位置:将 stale entry 移到 i 位置
// 这样做的目的是保持哈希表的顺序
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果向前扫描没找到 stale entry,就从当前位置开始清理
if (slotToExpunge == staleSlot) {
slotToExpunge = i;
}
// 执行清理
// 先清理 slotToExpunge 位置的 stale entry,然后启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果向后扫描发现新的 stale entry,且还没有设置清理起点
if (e.refersTo(null) && slotToExpunge == staleSlot) {
slotToExpunge = i; // 更新清理起点
}
}
// 没找到相同的 key,直接在 staleSlot 创建新 Entry
// 先释放旧的 value,帮助 GC 回收
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果运行中还有其他 stale entry,执行清理
if (slotToExpunge != staleSlot) {
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
}get 方法详解
get 方法执行流程图
get()
│
├─► 获取当前线程
│
├─► 获取线程的 threadLocals 字段
│ ├─► 存在?───是───► map.getEntry(this)
│ │ │
│ │ ├─► 计算哈希索引 i = hash & (len-1)
│ │ │
│ │ ├─► 直接命中?───是───► 返回 value
│ │ │
│ │ └─► 未命中,进入 getEntryAfterMiss
│ │ │
│ │ ├─► 线性探测查找
│ │ │ ├─► refersTo(key)?───是───► 返回 value
│ │ │ ├─► refersTo(null)?───是───► 清理
│ │ │ └─► 继续探测
│ │ │
│ │ └─► 遇到 null,返回 null
│ │
│ └─► 不存在或返回 null?───是───► setInitialValue()
│ │
│ ├─► 调用 initialValue() 获取初始值
│ ├─► 调用 set 方法设置初始值
│ ├─► 如果是 TerminatingThreadLocal,注册
│ └─► 返回初始值
│
└─► 结束get 方法入口源码详解
// ThreadLocal.java
public T get() {
// 调用私有方法,传入当前线程
// 这种设计允许子类重写行为
return get(Thread.currentThread());
}
private T get(Thread t) {
// 获取线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从 map 中获取 Entry
// getEntry 方法会处理直接命中、冲突查找等情况
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 没找到或 map 不存在,设置初始值
// 这是第一次调用 get 或之前被 remove 后的情况
return setInitialValue(t);
}getEntry 方法详解
// ThreadLocalMap.getEntry
// 这个方法优先处理快速路径:直接命中
// 这样设计是为了最大化直接命中的性能
private Entry getEntry(ThreadLocal<?> key) {
// 计算哈希位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 直接命中 - 使用 refersTo 进行高效比较
// 这是最常见的 case,希望尽可能快
if (e != null && e.refersTo(key)) {
return e; // 快速返回
} else {
// 未命中,进入线性探测查找
// 处理冲突或 stale entry 的情况
return getEntryAfterMiss(key, i, e);
}
}
// 冲突后的查找
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 线性探测直到遇到 null 槽
while (e != null) {
if (e.refersTo(key)) {
return e; // 找到了
}
if (e.refersTo(null)) {
// 发现 stale entry,立即清理
// 这是 get 操作中清理 stale entry 的时机
expungeStaleEntry(i);
} else {
// 继续下一个位置
i = nextIndex(i, len);
}
e = tab[i];
}
return null; // 没找到
}setInitialValue 方法详解
// 设置初始值
// 这是 get 操作中当值不存在时调用的方法
private T setInitialValue(Thread t) {
// 调用 initialValue() 获取初始值
// 用户可以重写这个方法提供自定义初始值
// 默认返回 null
T value = initialValue();
// 获取线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// map 存在,设置值
map.set(this, value);
} else {
// map 不存在,创建
createMap(t, value);
}
// 如果是 TerminatingThreadLocal,需要注册
// TerminatingThreadLocal 是需要在线程终止时执行清理的特殊 ThreadLocal
// 自动注册机制确保即使开发者忘记手动清理,也能释放资源
if (this instanceof TerminatingThreadLocal<?> ttl) {
TerminatingThreadLocal.register(ttl);
}
// 虚拟线程调试特性
if (TRACE_VTHREAD_LOCALS) {
dumpStackIfVirtualThread();
}
return value;
}
// 用户可以重写的 initialValue 方法
// 默认返回 null,子类可以重写提供初始值
protected T initialValue() {
return null;
}remove 方法详解
remove 方法执行流程图
remove()
│
├─► 获取当前线程
│
├─► 获取线程的 threadLocals 字段
│ ├─► 存在?───是───► map.remove(this)
│ │ │
│ │ ├─► 计算哈希索引 i = hash & (len-1)
│ │ │
│ │ ├─► 线性探测查找 Entry
│ │ │ │
│ │ │ └─► 找到后:
│ │ │ ├─► e.clear() 清除弱引用
│ │ │ └─► expungeStaleEntry(i) 清理槽位
│ │ │ │
│ │ │ ├─► 释放 value 引用
│ │ │ ├─► 槽位置 null
│ │ │ ├─► size--
│ │ │ └─► 重新哈希后续冲突的 Entry
│ │ │
│ │ └─► 返回
│ │
│ └─► 不存在?───是───► 直接返回
│
└─► 结束remove 方法入口源码详解
// ThreadLocal.java
public void remove() {
// 调用私有方法,传入当前线程
remove(Thread.currentThread());
}
private void remove(Thread t) {
// 获取线程的 ThreadLocalMap
ThreadLocalMap m = getMap(t);
if (m != null) {
// 调用 ThreadLocalMap 的 remove 方法
m.remove(this);
}
// 如果 map 不存在,说明没有值可删,直接返回
}ThreadLocalMap.remove 方法详解
// ThreadLocalMap.remove
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算哈希位置
int i = key.threadLocalHashCode & (len - 1);
// 线性探测查找
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 找到对应的 Entry
if (e.refersTo(key)) {
// 清除弱引用
// 调用 WeakReference.clear() 方法
// 这样 key 就不再被引用了
e.clear();
// 执行清理
// 释放 value,重新哈希后续的 entry
expungeStaleEntry(i);
return;
}
}
// 没找到,说明这个 ThreadLocal 在当前线程没有值
}expungeStaleEntry 清理方法详解
// 核心清理方法
// 这是 ThreadLocal 机制中最重要的清理方法
// staleSlot: 需要清理的 stale entry 的位置
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理 staleSlot 位置的 entry
// 1. 释放 value 的强引用,帮助 GC 回收
tab[staleSlot].value = null;
// 2. 将槽位置为 null,表示空槽
tab[staleSlot] = null;
// 3. 元素个数减1
size--;
// 重新哈希后续的 entry,直到遇到 null 槽
// 这一步非常重要:清理一个 stale entry 后,需要重新调整后续 entry 的位置
// 因为开放地址法中,entry 的位置依赖于它插入时的空槽
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 发现另一个 stale entry,也清理掉
e.value = null;
tab[i] = null;
size--;
} else {
// 有效的 entry,重新计算它应该在的位置
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; // 返回第一个 null 槽的位置
}cleanSomeSlots 启发式清理详解
// 启发式清理
// 这个方法执行对数级别的扫描,平衡性能和清理效果
// i: 开始扫描的位置
// n: 扫描控制参数,通常是元素个数或表长度
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// 下一个位置
i = nextIndex(i, len);
Entry e = tab[i];
// 发现 stale entry
if (e != null && e.refersTo(null)) {
n = len; // 重置扫描长度为 table 长度
removed = true;
// 执行彻底的清理
i = expungeStaleEntry(i);
}
} while ((n >>>= 1) != 0); // n 每次右移 1 位,对数级别的扫描
return removed;
}
/*
扫描次数示例:
- 如果 n=16,二进制 10000,右移 4 次后为 0
- 所以最多扫描 log2(n) + 1 次
- 但如果发现 stale entry,n 重置为 len,可能扫描更多
- 这种设计平衡了清理效果和性能开销:
* 不扫描:快速但保留垃圾
* 扫描全部:能找到所有垃圾但可能 O(n)
* 对数扫描:折中方案,既不会太频繁也不会太久不清理
*/扩容机制详解
// 扩容前的准备
private void rehash() {
// 全面清理所有的 stale entry
// 先清理再扩容,可以减少需要迁移的数据量
expungeStaleEntries();
// 判断是否需要扩容
// 使用更低的阈值 threshold - threshold/4 = threshold * 0.75
// 比正常的 threshold 更严格,避免频繁扩容
if (size >= threshold - threshold / 4) {
resize();
}
}
// 全面清理所有 stale entry
// 遍历整个表,清理每一个 stale entry
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.refersTo(null)) {
expungeStaleEntry(j);
}
}
}
// 真正的扩容
// 将容量扩大为原来的2倍
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2; // 2倍扩容
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历旧表
for (Entry e : oldTab) {
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
// stale entry,只释放 value,不迁移
// 帮助 GC 回收
e.value = null;
} else {
// 有效 entry,重新哈希到新表
int h = k.threadLocalHashCode & (newLen - 1);
// 线性探测找到空槽
while (newTab[h] != null) {
h = nextIndex(h, newLen);
}
newTab[h] = e; // 放入新表
count++;
}
}
}
// 设置新阈值
setThreshold(newLen);
size = count;
table = newTab; // 替换为扩容后的新表
}内存泄漏预防机制
/*
ThreadLocal 通过多种机制协同工作来预防内存泄漏:
1. 弱引用键 (WeakReference keys)
- Entry 继承 WeakReference,ThreadLocal 作为弱引用键
- 当 ThreadLocal 不再被其他强引用持有时,可以被 GC 回收
- 回收后 key 变为 null,形成 stale entry
- 这是防止 ThreadLocal 实例泄漏的第一道防线
2. 探测式清理 (expungeStaleEntry)
- 在 get、set、remove 操作时触发
- 清理发现的 stale entry (key 为 null 的 Entry)
- 释放 value 的强引用,帮助 GC 回收
- 重新哈希后续的 entry,保持哈希表健康
3. 启发式清理 (cleanSomeSlots)
- 对数级别的扫描,平衡性能和清理效果
- 在 set 操作后调用,检查是否有 stale entry
- 如果发现 stale entry,会触发更彻底的清理
- 既不会太频繁影响性能,也不会太久不清理
4. 全面清理 (expungeStaleEntries)
- 在 rehash 时触发
- 遍历整个表,清理所有 stale entry
- 为扩容做准备,减少迁移的数据量
5. 主动 remove (user-initiated removal)
- 用户代码主动调用 remove() 方法
- 最可靠的清理方式
- 应该在 finally 块中保证执行
- 示例:
ThreadLocal<Connection> connLocal = new ThreadLocal<>();
try {
connLocal.set(openConnection());
// 业务逻辑
} finally {
connLocal.remove(); // 确保清理
}
6. TerminatingThreadLocal 自动注册
- JDK 21 新增特性
- 需要在线程终止时执行清理的特殊 ThreadLocal
- 自动注册到全局注册表,线程退出时触发清理
- 用于需要主动释放资源的场景,如文件句柄、网络连接
*/