ThreadLocalMap
ThreadLocalMap
文章目录
- ThreadLocalMap
- 1. `ThreadLocal` 的基础结构
- 1.1 类继承关系
- 1.2 `ThreadLocalMap` 内部类概述
- 1.3 弱引用键的设计理念
- 2. `ThreadLocalMap`的实现机制
- 2.1 Entry结构与弱引用
- 2.2 哈希表设计
- 2.3 解决哈希冲突的策略
- 3. `ThreadLocalMap`的关键操作源码分析
- 3.1 set方法实现
- 3.2 get方法实现
- 3.3 remove方法实现
- 3.4 过期Entry的清理机制
- 4. 核心操作源码分析
- 4.1 `ThreadLocal`.set方法实现
- 4.2 `ThreadLocal.get`方法实现
- 4.3 `ThreadLocal.remove`方法实现
- 4.4 `ThreadLocal.withInitial`方法
- 5. 内存泄漏问题深度剖析
- 5.1 为什么会发生内存泄漏
- 5.3 源码中的防泄漏设计
- 5.4 清理机制的触发时机
你是否曾遇到这样的场景:一个对象需要在同一个线程内的多个方法中传递,却不想把它作为参数在每个方法间传来传去?或者你需要保存一个"线程上下文"信息,比如用户身份、事务ID或请求追踪信息?
在并发的世界里,全局变量是危险的。正如一个热闹的宿舍里,把你的私人物品放在公共区域,总会有人"不小心"把它们挪走。ThreadLocal
就像是给每个线程发了一个专属保险柜,只有线程自己能存取,线程间互不干扰。
然而,这个看似简单的工具类背后隐藏着巧妙的设计和潜在的风险。Java面试中,ThreadLocal
的原理和内存泄漏问题"几乎成了高频标配题。许多开发者对ThreadLocal
的印象仅限于"每个线程拥有独立的变量副本",但对其内存模型、实现机制和正确使用姿势知之甚少。
1. ThreadLocal
的基础结构
1.1 类继承关系
我们来看一下ThreadLocal
的类定义:
public class ThreadLocal<T> {// 实现代码...
}
ThreadLocal
是一个泛型类,类型参数T表示它可以存储任何类型的对象。相比其他并发工具类,ThreadLocal
的类结构非常简单,它没有继承任何类,也没有实现任何接口。
这种设计反映了它的专注性——只做一件事:为每个线程提供独立的变量副本。
1.2 ThreadLocalMap
内部类概述
ThreadLocal
最核心的部分是其内部私有静态类ThreadLocalMap
:
static class ThreadLocalMap {/*** Entry继承自WeakReference,key是ThreadLocal对象的弱引用*/static class Entry extends WeakReference<ThreadLocal<?>> {/** 与这个ThreadLocal关联的值 */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}/** 初始容量 - 必须是2的幂 */private static final int INITIAL_CAPACITY = 16;/** 存放数据的table,大小必须是2的幂 */private Entry[] table;/** table中的entry数量 */private int size = 0;/** 扩容阈值,默认为0 */private int threshold = 0;// 其他字段和方法...
}
乍一看,这个ThreadLocalMap
结构很像HashMap
,都是基于哈希表实现的。但有几个关键区别:
-
ThreadLocalMap
是ThreadLocal
的静态内部类,但它的实例是保存在Thread类中的 -
Entry的key是
ThreadLocal
对象的弱引用,这对内存管理非常重要 -
ThreadLocalMap
没有实现Map接口,是一个定制的哈希表
1.3 弱引用键的设计理念
ThreadLocalMap
中使用弱引用来保存ThreadLocal
对象,这是一个精妙的设计:
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k); // 将ThreadLocal对象作为弱引用value = v;}
}
为什么要用弱引用?想象这样一个场景:
void someMethod() {ThreadLocal<User> userThreadLocal = new ThreadLocal<>();userThreadLocal.set(new User("John"));// 使用userThreadLocal// 方法结束,userThreadLocal变量不再可访问
}
当方法执行完毕,局部变量userThreadLocal就会被销毁,但是Thread对象中的ThreadLocalMap
仍然持有对这个ThreadLocal
的引用。如果是强引用,那么即使userThreadLocal变量已经不可访问,ThreadLocal
对象也不会被垃圾回收,这可能导致内存泄漏。
通过使用弱引用,一旦外部不再持有ThreadLocal
的强引用,垃圾回收器就可以回收这个ThreadLocal
对象,即使它仍被ThreadLocalMap
引用。
但是,此时Entry中的value还是强引用,不会被自动回收,这就是常说的ThreadLocal
内存泄漏的根源。后面我们会详细讨论这个问题。
2. ThreadLocalMap
的实现机制
2.1 Entry结构与弱引用
ThreadLocalMap
内部使用Entry数组来存储数据:
private Entry[] table;static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
每个Entry包含:
- 一个
ThreadLocal
的弱引用作为key - 一个普通的强引用Object作为value
值得注意的是,WeakReference是JDK提供的一种特殊引用类型,当一个对象只剩下弱引用指向它时,垃圾回收器会在下一次GC时回收该对象。
// 弱引用的基本使用
ThreadLocal<?> threadLocal = new ThreadLocal<>();
WeakReference<ThreadLocal<?>> weakRef = new WeakReference<>(threadLocal);// 如果此时threadLocal = null,那么weakRef.get()在下一次GC后可能返回null
threadLocal = null;
System.gc(); // 触发GC
ThreadLocal<?> referent = weakRef.get(); // 可能已经是null
这种设计让ThreadLocal
实例在不再需要时可以被回收,同时Entry中的value依然存在,直到Thread结束或者手动调用remove()方法。
2.2 哈希表设计
ThreadLocalMap
使用开放地址法解决哈希冲突:
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn 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);elsei = nextIndex(i, len);e = tab[i];}return null;
}private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}
与HashMap
使用链表或红黑树解决冲突不同,ThreadLocalMap
使用线性探测法:如果计算出的槽位已被占用,就继续往后查找,直到找到空槽位或找到目标Entry。
ThreadLocalMap
中每个ThreadLocal
的哈希值通过一个特殊的原子递增生成器计算:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
这里使用的增量0x61c88647是一个神奇的数字,它是斐波那契散列乘数,能够使哈希值均匀分布,减少冲突。
2.3 解决哈希冲突的策略
当发生哈希冲突时,ThreadLocalMap
采用线性探测法来解决:
private void set(ThreadLocal<?> key, Object value) {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)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}
这段代码展示了ThreadLocalMap
如何处理冲突:
-
计算初始槽位i
-
如果该位置已被占用,检查key是否相同
- 如果key相同,更新value并返回
- 如果key为null(已被GC回收),替换这个"陈旧"的Entry
- 否则,继续查找下一个位置
-
如果找到空槽位,插入新Entry
-
增加size并检查是否需要清理或扩容
值得注意的是,在查找过程中如果发现key为null的Entry(表示ThreadLocal
已被回收),会主动清理这些Entry,这是一种"顺手清理"的机制,有助于减少内存泄漏。
3. ThreadLocalMap
的关键操作源码分析
3.1 set方法实现
set方法是ThreadLocalMap
最核心的方法之一,用于设置键值对:
private void set(ThreadLocal<?> key, Object value) {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)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}
这段代码处理了三种情况:
-
找到相同的key,更新value
-
找到key为null的"陈旧"Entry,替换它
-
找到空槽位,插入新Entry
其中第2点很重要,这是ThreadLocalMap
自动清理机制的一部分。当检测到key为null的Entry时,调用replaceStaleEntry方法替换并清理:
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;// 向前扫描,查找更多的陈旧Entryint slotToExpunge = staleSlot;for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) {if (e.get() == null)slotToExpunge = i;}// 向后遍历for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 如果找到key,与staleSlot位置的Entry交换if (k == key) {e.value = value;tab[i] = tab[staleSlot];tab[staleSlot] = e;// 如果在前面的扫描中没有找到陈旧条目,则起点为这里if (slotToExpunge == staleSlot)slotToExpunge = i;// 清理陈旧的EntrycleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 如果我们还没有找到staleSlot之前的陈旧条目if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// 如果我们没有找到key,把新的Entry放入staleSlottab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// 如果有其他的陈旧Entry,清理它们if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
这个方法相当复杂,它不仅替换了陈旧的Entry,还会主动清理其他陈旧Entry,以减少内存泄漏的可能性。
3.2 get方法实现
get方法用于获取与ThreadLocal
关联的值:
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn 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);elsei = nextIndex(i, len);e = tab[i];}return null;
}
get方法首先计算哈希索引,然后检查该位置的Entry。如果key不匹配或为null,则调用getEntryAfterMiss继续查找。
同样,在查找过程中如果发现key为null的Entry,会调用expungeStaleEntry进行清理,这是另一个"顺手清理"的时机。
3.3 remove方法实现
remove方法用于移除ThreadLocal
关联的值:
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)]) {if (e.get() == key) {e.clear(); // 清除引用expungeStaleEntry(i); // 清理这个槽位及后续陈旧的Entryreturn;}}
}
remove方法首先查找key对应的Entry,找到后调用Entry.clear()方法清除ThreadLocal
的弱引用,然后调用expungeStaleEntry清理这个槽位及后续可能的陈旧Entry。
这个方法是彻底清除ThreadLocal
数据的正确方式,它不仅移除了value的强引用,还处理了可能的连锁效应。
3.4 过期Entry的清理机制
ThreadLocalMap
内部实现了多种清理机制来处理过期(陈旧)的Entry:
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 清除指定槽位的Entrytab[staleSlot].value = null;tab[staleSlot] = null;size--;// 重新哈希后续的EntryEntry e;int i;for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// 如果需要,重新放置Entry到正确位置while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;
}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];if (e != null && e.get() == null) {n = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;
}private void rehash() {expungeStaleEntries(); // 完全清理所有陈旧Entry// 如果大小仍然超过阈值的3/4,则扩容if (size >= threshold - threshold / 4)resize();
}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.get() == null)expungeStaleEntry(j);}
}
ThreadLocalMap
提供了多个级别的清理机制:
-
定点清理:expungeStaleEntry方法清理指定位置的陈旧Entry,并重新哈希后续Entry
-
探测式清理:cleanSomeSlots方法在有限步数内查找并清理陈旧Entry
-
全量清理:expungeStaleEntries方法扫描整个表,清理所有陈旧Entry
这些清理机制在不同操作中被触发:
- set操作可能触发探测式清理和rehash
- get操作在查找过程中可能触发定点清理
- remove操作总是触发定点清理
- 当size达到阈值时,会触发全量清理和可能的扩容
通过这些多层次的清理机制,ThreadLocalMap
尽量减少内存泄漏的可能性,但并不能完全避免。
4. 核心操作源码分析
4.1 ThreadLocal
.set方法实现
ThreadLocal
的set方法是我们直接使用的API
,看看它是如何实现的:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这个方法非常简洁:
-
获取当前线程对象
-
尝试获取线程中的
ThreadLocalMap
-
如果map存在,调用map.set设置值
-
如果map不存在,创建一个新的
ThreadLocalMap
关键点在于:ThreadLocalMap
实例是存储在Thread对象中的,而不是ThreadLocal
对象中。每个Thread都有自己的ThreadLocalMap
,用于存储所有与该线程关联的ThreadLocal
变量。
这就是ThreadLocal
能够实现线程隔离的核心机制——数据实际上是存储在线程内部的。
4.2 ThreadLocal.get
方法实现
get方法用于获取当前线程下的变量值:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();
}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}protected T initialValue() {return null;
}
get方法的逻辑是:
-
获取当前线程的
ThreadLocalMap
-
如果map存在,尝试获取this作为key的Entry
-
如果Entry存在,返回其value
-
否则,调用setInitialValue设置并返回初始值
initialValue方法默认返回null,但可以被子类重写以提供自定义的初始值。这是一个很有用的特性,比如可以用来定义线程本地变量的默认值。
4.3 ThreadLocal.remove
方法实现
remove方法用于移除当前线程中的变量:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
这个方法同样简单明了:获取当前线程的ThreadLocalMap
,如果存在则从中移除this对应的Entry。
正确调用remove方法对防止内存泄漏至关重要,尤其是在线程池环境中。线程被重用时,如果不清理ThreadLocal
变量,新的任务可能会访问到之前任务设置的值,既造成了内存泄漏,又可能导致逻辑错误。
4.4 ThreadLocal.withInitial
方法
从Java 8开始,ThreadLocal
增加了一个工厂方法withInitial:
return new SuppliedThreadLocal<>(supplier);
}static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {private final Supplier<? extends T> supplier;SuppliedThreadLocal(Supplier<? extends T> supplier) {this.supplier = Objects.requireNonNull(supplier);}@Overrideprotected T initialValue() {return supplier.get();}
}
这个方法创建一个特殊的ThreadLocal
子类,它的initialValue方法使用提供的Supplier来获取初始值。这提供了一种更简洁的方式来创建带初始值的ThreadLocal
:
// 旧方式
ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}
};// 新方式
ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
这种函数式风格更加简洁,也更符合Java 8的编程习惯。
5. 内存泄漏问题深度剖析
5.1 为什么会发生内存泄漏
ThreadLocal
的内存泄漏问题是众所周知的,让我们深入分析这个问题:
Thread实例 -> ThreadLocalMap -> Entry -> value-> Entry -> value-> Entry -> value (key为null)
当ThreadLocal
对象不再被引用时,Entry中的key变成null(因为是弱引用),但value仍然被强引用。如果线程长时间存活(如线程池中的线程),这些无法访问的value就会一直占用内存,形成泄漏。
具体来说,泄漏发生的条件是:
-
ThreadLocal对象不再被外部引用
-
线程一直存活
-
没有手动调用remove方法清理
在这种情况下,虽然ThreadLocal
对象会被回收,但其关联的value不会被回收,而且无法再通过正常手段访问到这些value。
5.2 弱引用与内存泄漏的关系
ThreadLocalMap
中的Entry继承自WeakReference,key是ThreadLocal
的弱引用:
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
弱引用的特性是:当对象只被弱引用引用时,垃圾回收器会在下一次回收中回收该对象。这意味着当ThreadLocal
对象不再被外部强引用时,Entry中的key会变成null。
但这里有一个问题:虽然key会被回收,但value是强引用,不会自动回收。如果没有其他机制处理这些key为null的Entry,就会导致value无法被回收,形成内存泄漏。
ThreadLocalMap
的设计者意识到了这个问题,所以在get、set、remove等操作中添加了清理机制,但这些清理是"顺手"进行的,不保证及时清理所有过期Entry。
5.3 源码中的防泄漏设计
ThreadLocalMap
中包含多种防止内存泄漏的设计,我们前面已经分析过:
-
在set过程中清理:发现key为null的Entry时,会调用replaceStaleEntry或expungeStaleEntry进行清理
-
在get过程中清理:getEntryAfterMiss方法中,发现key为null时会调用expungeStaleEntry清理
-
在remove时清理:remove方法直接调用expungeStaleEntry清理当前和后续的过期Entry
-
在扩容前全量清理:rehash方法首先调用expungeStaleEntries清理所有过期Entry
这些清理机制都是为了减少内存泄漏的可能性,但它们都有一个共同的问题:必须有人调用ThreadLocalMap
的方法才会触发清理。如果一个线程不再使用任何ThreadLocal
,但线程本身还在运行,那么这些过期的Entry就不会被自动清理。
5.4 清理机制的触发时机
让我们总结一下ThreadLocalMap
中清理机制的触发时机:
-
set时触发:向
ThreadLocalMap
中设置值时,可能触发清理 -
get时触发:获取值时,如果发生哈希冲突,可能触发清理
-
remove时触发:移除值时,必定触发清理
-
resize时触发:扩容前,会进行全量清理
这些清理机制虽然可以减少内存泄漏的可能性,但都不能完全避免泄漏。最安全的做法仍然是在不再需要ThreadLocal
变量时,显式调用remove方法进行清理。
特别是在线程池环境中,线程结束任务后会被重用,如果不清理ThreadLocal
,可能导致数据污染和内存泄漏。因此,ThreadLocal
变量的生命周期应该与任务的生命周期一致,而不是与线程的生命周期一致。