一、核心数据结构:Thread 与 ThreadLocalMap
一切始于 Thread 类。每个 Thread 对象内部都持有两个非常重要的字段:
java
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
threadLocals:存储本线程所有的普通ThreadLocal变量。inheritableThreadLocals:存储可被子线程继承的InheritableThreadLocal变量。
ThreadLocalMap 是 ThreadLocal 的一个静态内部类 ,它是整个机制的核心。它本质上是一个定制化的、使用线性探测法解决哈希冲突的哈希表 。它没有实现 Map 接口。
ThreadLocalMap 的内部结构
java
static class ThreadLocalMap {
// 哈希表中的条目(Entry)类,继承自WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
// The value associated with this ThreadLocal.
Object value;
// 键Key是弱引用的ThreadLocal,值Value是强引用的实际数据
Entry(ThreadLocal<?> k, Object v) {
super(k); // 调用WeakReference的构造方法,使Key成为弱引用
value = v;
}
}
// 哈希表的初始容量,必须是2的幂
private static final int INITIAL_CAPACITY = 16;
// 底层的Entry数组,表的大小也必须是2的幂
private Entry[] table;
// 表中条目的数量
private int size = 0;
// 扩容阈值,通常是 table.length * 2/3
private int threshold;
// ... 其他方法(set, get, remove, rehash等)
}
最关键的点:Entry 的 Key 是弱引用(WeakReference)。
super(k)将ThreadLocal实例包装成了一个弱引用。value是一个普通的强引用。
这既是设计巧妙之处,也是内存泄漏风险的根源。 当外部的强引用(如 threadLocalRef = null)消失后,这个 ThreadLocal 对象只剩下一个来自 Entry 的弱引用。在下一次垃圾回收时,这个 Key 就会被回收,从而变成 null。但 Entry 本身和它的 value 还在数组中,除非这个 Entry 被主动清理。
二、set(T value) 方法详解
java
public void set(T value) {
Thread t = Thread.currentThread();
// 1. 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 2. 如果Map已存在,则以当前ThreadLocal实例为key,设置value
map.set(this, value);
} else {
// 3. 如果Map不存在(第一次调用set),则创建Map并设置初始值
createMap(t, value);
}
}
核心逻辑在 map.set(this, value) 中,即 ThreadLocalMap.set(ThreadLocal<?> key, Object value)。
java
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 1. 计算哈希槽位:使用神奇的哈希算法 & (len-1)
int i = key.threadLocalHashCode & (len - 1);
// 2. 线性探测:从计算出的位置i开始,遍历数组寻找合适的槽位
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); // 获取Entry的Key(弱引用指向的ThreadLocal对象)
// 2.1 情况A:找到了相同的Key(引用相等)
if (k == key) {
e.value = value; // 直接覆盖value
return;
}
// 2.2 情况B:找到了一个"陈旧条目"(Stale Entry)------ Key已被GC回收,变为null
if (k == null) {
// 这是一个关键的清理操作!用新值替换这个陈旧的条目
replaceStaleEntry(key, value, i);
return;
}
// 2.3 情况C:哈希冲突,继续向下一个位置探测
}
// 3. 情况D:找到了一个空槽位(null)
tab[i] = new Entry(key, value);
int sz = ++size;
// 4. 清理一些陈旧的条目,如果清理后size仍然>=阈值,则进行rehash扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set 方法的关键点:
- 哈希计算:
threadLocalHashCode是一个每次创建ThreadLocal实例时都会递增的静态原子变量,保证了哈希值的均匀分布。 - 主动清理: 在情况B 和步骤4 中,
set方法会主动探测并清理那些Key == null的陈旧条目(replaceStaleEntry和cleanSomeSlots) 。这是ThreadLocal在 API 层面自我修复、减少内存泄漏的机制。但这只是"尽力而为",不能保证完全清理。
三、get() 方法详解
java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从Map中获取当前ThreadLocal对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果Map为null 或 Entry为null,则进行初始化
return setInitialValue();
}
核心逻辑在 map.getEntry(ThreadLocal<?> key)。
java
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 1. 快速路径:槽位i的Entry不为null且Key匹配,直接返回
if (e != null && e.get() == key)
return e;
else
// 2. 慢速路径:可能发生了哈希冲突或Key被回收,进行线性探测查找
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();
if (k == key)
return e; // 找到了,返回
if (k == null)
expungeStaleEntry(i); // 关键!又遇到了陈旧条目,触发清理
else
i = nextIndex(i, len); // 继续探测
e = tab[i];
}
return null; // 没找到
}
get 方法的关键点:
- 主动清理: 在慢速路径中,一旦在探测过程中遇到
k == null的陈旧条目,会立即调用expungeStaleEntry(i)方法清理该位置及其后续连续段内的所有陈旧条目。这意味着get()调用也可能触发内存回收。 - 初始化: 如果最终没找到值,会调用
setInitialValue(),其逻辑与set()类似,只是初始值由initialValue()方法提供。
四、remove() 方法详解
这是最直接、最彻底的清理方法。
java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
// ThreadLocalMap.remove(ThreadLocal<?> key)
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
// 线性探测寻找Key
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 1. 显式地将Entry的Key引用清除(虽然它已经是弱引用,但这是明确断开)
e.clear();
// 2. 显式地将Value引用清除!这是防止内存泄漏最关键的一步!
// 同时,这个方法还会进行清理以消除陈旧条目。
expungeStaleEntry(i);
return;
}
}
}
expungeStaleEntry(int staleSlot) 是核心清理方法,它做了以下几件事:
- 将
staleSlot位置的 Entry 的value显式置为null,帮助 GC。 - 将
staleSlot位置的 Entry 本身置为null。 - 重新哈希(rehash)该位置后续连续段内的所有非陈旧条目,以优化查找效率。
remove() 是唯一能保证立即、彻底清理掉当前 ThreadLocal 对应 Value 的方法。
五、总结与内存泄漏根源
从源码我们可以得出清晰的结论:
-
内存泄漏的根源 :
ThreadLocalMap.Entry的Key是弱引用,Value是强引用。当ThreadLocal外部强引用消失后,Key被 GC 回收变为null,但Value由于仍然被Entry强引用,而Entry又被ThreadLocalMap的table数组强引用,导致Value无法被回收。如果线程不死(如线程池),这个无用的Value就一直占着内存。 -
自我修复的尝试 :
ThreadLocalMap在设计上并非"躺平",它在set()和get()方法中会顺带 进行清理(cleanSomeSlots,expungeStaleEntry)。但这是一种"惰性"清理,只有在调用这些方法且恰好碰到陈旧条目时才会触发。如果之后不再调用set或get,这些泄漏的内存就永远无法被回收。 -
最佳实践的必然性 :因此,必须手动调用
remove()。它是唯一能提供确定性清理的手段,直接调用最彻底的expungeStaleEntry方法,确保Value被释放。尤其是在使用线程池时,remove()不仅是为了防止内存泄漏,更是为了防止数据污染。
源码告诉我们:永远不要依赖 ThreadLocal 的自我清理机制,主动调用 remove() 是开发者不可推卸的责任。