
探究 ThreadLocal 原理
1、ThreadLocal 的基本原理
ThreadLocal 的 set(T value) 方法,会将 value 保存在 Thread 类的 ThreadLocalMap 变量中,而 ThreadLocalMap 又是 ThreadLocal 的内部类:
package java.lang;
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap inheritableThreadLocals;
}ThreadLocal 的 set(T value) 方法保存专属于线程副本的原理如下:
package java.lang;
public class ThreadLocal<T> {
...
public void set(T value) {
set(Thread.currentThread(), value);
if (TRACE_VTHREAD_LOCALS) {
dumpStackIfVirtualThread();
}
}
private void set(Thread t, T value) {
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
// 注意,这里的 threadLocals 是 Thread 类中 ThreadLocalMap 类型的变量
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...
}ThreadLocalMap 为什么是弱引用:
package java.lang;
public class ThreadLocal<T> {
...
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
// 从这句可以看出 ThreadLocalMap 保存的 Entry 对象,key 为 super(k),
// 即调用父类 WeakReference 的构造方法,形成了弱引用
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
// 注意这里的构造方法,在前文的 set(T value) 位置被调用了
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 这里将 Entry 对象存入了 table 中
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
...
}
}从 ThreadLocal 中取回存入的对象:
public class ThreadLocal<T> {
...
private T get(Thread t) {
ThreadLocalMap map = getMap(t);
if (map != null) {
// !!注意这个 map 是 ThreadLocalMap 类型的
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
return setInitialValue(t);
}
static class ThreadLocalMap {
...
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
// 还记得这个 table 吗?看看前文,这里保存的是当前线程所有的
// ThreadLocalMap,因为每个 Thread 实例中都包含一个私有的 ThreadLocalMap
Entry e = table[i];
if (e != null && e.refersTo(key))
return e;
else
return getEntryAfterMiss(key, i, e);
}
}
}2、为什么 Entry 的 key 用弱引用?
如果 key 是强引用,即使你不再使用这个 ThreadLocal 对象(没有任何地方引用它),它依然会被 ThreadLocalMap 中的 Entry 强引用着,此时 ThreadLocal 对象无法被回收。如果该线程是一个线程池中的线程,它迟迟不被销毁,那么它内部的 ThreadLocalMap 也会一直存在,导致 ThreadLocal 以及它绑定的 value 一直无法回收,这就造成了内存泄漏。
思考:ThreadLocal 对象即使没被使用,它依然会被 ThreadLocalMap 中的 Entry 强引用着,此时 ThreadLocal 对象无法被回收,这是为什么呢?
因为 ThreadLocalMap 的生命周期和 Thread 相同,所以 ThreadLocalMap 对象不会被回收,因为 Thread 是 GC Root
如果 key 是弱引用:
一旦外部不再引用这个 ThreadLocal,它就会被 GC 掉。虽然 ThreadLocalMap 中的 Entry 还在,但它的 key 已经是 null;接下来,ThreadLocalMap 在后续的 get() 或 set() 操作中,会检测到这些 key 为 null 的条目,并通过内部的清理机制将其移除,以避免内存泄漏。
3、为什么没有调用 remove() 会导致内存泄漏?
当你在使用
ThreadLocal,但没有调用remove()时,很容易造成 内存泄漏。
1️⃣ 为什么 remove() 是必要的?
如果不调用 remove(),这个 Entry 会一直保留在 ThreadLocalMap 中;即便 ThreadLocal 本身在代码中被丢弃,Key 可能被 GC 清理(成为 stale entry),但value 依然强引用在 map 中,直到强制清理或重建 map 时才释放。在线程池的场景中,线程被复用,当线程长时间空闲时,这些 value 永远不会被收回,最终造成内存堆积。因为清理机制必须依赖对 ThreadLocal 的访问触发。
2️⃣ 清理机制的触发条件
ThreadLocalMap 中存在清理机制,比如 expungeStaleEntries() 会在 get()/set() 操作时被调用,但它只有在后续访问 ThreadLocalMap 时触发。如果一个线程执行后不再触碰 ThreadLocal,这些 Entry 也不会主动清除,对应的 value 就会一直占着内存直至线程结束。
