深入剖析Java中ThreadLocal原理
1. ThreadLocal
的概念
ThreadLocal
是 Java 中的一个类,用于实现线程本地存储。它允许你创建一个变量,这个变量在每个线程中都有自己独立的副本,不同线程之间的副本互不干扰。换句话说,ThreadLocal
提供了一种线程隔离的机制,使得每个线程都可以拥有自己的变量实例。
1.1 ThreadLocal
的基本用法
ThreadLocal<String> threadLocal = new ThreadLocal<>();// 在主线程中设置值
threadLocal.set("Main Thread Value");// 在子线程中设置值
new Thread(() -> {threadLocal.set("Child Thread Value");Log.d("ThreadLocal", "Child Thread: " + threadLocal.get()); // 输出: Child Thread Value
}).start();// 在主线程中获取值
Log.d("ThreadLocal", "Main Thread: " + threadLocal.get()); // 输出: Main Thread Value
1.2 ThreadLocal
的工作原理
ThreadLocal
内部维护了一个 ThreadLocalMap
,这个 ThreadLocalMap
是每个线程私有的。当你调用 ThreadLocal.set(value)
时,实际上是将 value
存储在当前线程的 ThreadLocalMap
中,键是 ThreadLocal
实例本身。当你调用 ThreadLocal.get()
时,会从当前线程的 ThreadLocalMap
中获取与 ThreadLocal
实例关联的值。
2. ThreadLocal
剖析
概念很简单,一看就会,但让你详细介绍下其原理,那可能就懵比了,问原理,我会概念:)
既然,ThreadLocal 是用于实现线程本地存储,那么它和 Thread 的关系是怎样的呢?
ThreadLocal
的数据之所以与线程绑定,是因为其内部实现依赖于每个线程私有的存储空间。具体来说,ThreadLocal
利用了线程内部的 ThreadLocalMap
来存储和管理每个线程独立的数据副本。
public class Thread implements Runnable {// 每个线程私有的 ThreadLocalMapThreadLocal.ThreadLocalMap threadLocals;// 其他成员和方法...
}
2.1 每个线程都有自己的 ThreadLocalMap
在Java中,每个 Thread 都有一个名为 threadLocals
的成员变量,类型为 ThreadLocal.ThreadLocalMap
,它是线程私有的,其他线程无法访问。ThreadLocalMap 是一个自定义的哈希表,其Entry
结构如下:
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
可以看出,Entry 继承自 WeakReference<ThreadLocal<?>>
,这意味着对 ThreadLocal 实例的引用是弱引用,而 value
则是强引用,这个就是 ThreadLocal 会造成内存泄漏的关键点,后面详细分析。
弱引用(
WeakReference
)在垃圾回收(GC)中的行为如下:
• 弱引用对象:如果一个对象只被弱引用所引用,而没有强引用(Strong Reference
)指向它,那么在下一次垃圾回收时,这个对象就会被回收掉。
• 应用场景:弱引用常用于实现缓存等需要在不阻止对象被回收的情况下引用对象的情况。
在 ThreadLocalMap
中,使用弱引用可以确保当没有其他强引用指向某个 ThreadLocal
实例时,该实例可以被垃圾回收,从而避免因为 ThreadLocal
实例本身无法被回收而导致整个 Entry
一直存在。
2.2 ThreadLocal 存储数据
通过ThreadLocal
的 set
方法可以看到 set 值的时候,通过获取当前线程 获取 ThreadLocalMap
,threadLocals
,如果获取到了 就拿当前的 ThreadLocal
为键,存储当前值,如果为null,就代表还为初始化,就初始化赋值,然后将 ThreadLocal
实例作为键,将 value 作为值,存储到 threadLocals
中。
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);
}
2.3 ThreadLocal 存储数据
get
值的时候,同样是获取当前线程 获取 ThreadLocalMap
即 threadLocals
,如果获取到了,就用当前的 ThreadLocal
去拿对应的值,为null 不存在,则返回初始值,内部就是 初始化 threadLocals
和存入并返回null值。
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();
}
可以看出,之所以与线程这么强相关,就是每次读取都要通过Thread.currentThread()
获取当前线程,然后再去操作数据,而所有的数据都在 Thread
中的 threadLocals
进行存储,这下是不是瞬间清楚它们的关系了。
2.4 线程隔离的机制
因为每个线程都有自己的 threadLocals ,并且
ThreadLocal以自身为键,存储数据,每个线程都可以独立获取自己的
ThreadLocal` 值,线程之间互不影响,对其他线程数据不可见,确保线程存储私有性。
3. 关于 ThreadLocal 会导致内存泄漏问题
先来看下ThreadLocalMap
的结构
• 键(Key):ThreadLocal
实例,通过 WeakReference
引用。
• 值(Value):存储的对应值,强引用。
弱引用的特性:如果一个对象只被弱引用指向,而没有强引用指向它,那么在下一次垃圾回收时,这个对象会被回收。
注意是只被!代表其他地方已经不再使用了,已经是垃圾了,才会被回收,而不当GC到来时,弱引用对象就没了,我之前对此有过疑惑,再这里再强调下。
根据内存泄漏定义:长生命周期对象引用了短生命周期对象可知:ThreadLocalMap
的 key
是 ThreadLocal
的弱引用(WeakReference),但 value
是强引用,如果 ThreadLocal
本身被 GC 回收了,但 Thread
一直存活(如线程池中的线程),那么 value
就不会被清除,从而造成内存泄漏。
引用链示意图
Thread (线程池中的线程)
└── threadLocals : ThreadLocalMap└── Entry[] table└── Entry : WeakReference<ThreadLocal> → null (ThreadLocal 已被回收)└── value → [object] (value 强引用未被清理)
这个 value
虽然没有人用了,但由于它还在 ThreadLocalMap
中,所以不会被 GC,造成内存泄漏。
解决的办法也很简单,手动调用下 ThreadLocal.remove()
。
最后再明确一下概念,每个ThreadLocal
只保存一个值,多次 set 不会叠加数据,而是会替换原有值,真正的内存泄漏风险来自未清理的 value
而非 set
操作本身,由于ThreadLocal
是直接操作当前线程,数据的存储最终是保存在 Thread
的 ThreadLocalMap
中,由 threadLocals
统一管理。
结束了么?并没有,既然要剖析,那就再多了解一点吧;
4. ThreadLocal 中 set 方法源码分析
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; // 情况1:键已存在,直接更新值return;}if (k == null) { // 情况2:发现过期EntryreplaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value); // 情况3:新增Entryint sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}
ThreadLocalMap
使用开放地址法解决哈希冲突。
从初始索引开始,遍历 ThreadLocalMap
中的槽位,查找是否已经存在相同的 ThreadLocal
键。
主要包括三种处理情况:键存在、键过期、键不存在。
注意ThreadLocal<?> k = e.get();
其中 e.get()
是获取 ThreadLocal
的引用,如果已经被GC回收了,则返回null,如果遇到 key
为 null
的 Entry
,表示这是一个过期的条目(由于弱引用被回收),调用 replaceStaleEntry 方法来替换这个过期条目,注意,replaceStaleEntry
内部仍会调用 cleanSomeSlots
进行尝试清理链表,维持哈希表结构。
最后是尝试清理一些过期的条目,如果清理后仍然达到阈值,则进行扩容操作。
所以可知,即使不显式调用 remove
方法的情况下,ThreadLocalMap
内部的 cleanSomeSlots
方法仍然有可能帮助清理过期的数据,只是有概率会,但这并不保险,最好还是手动调用 remove
方法。
再补充个知识点,ThreadLocalMap
在插入数据时 也会产生哈希冲突, 但于 HashMap
的方式有相似之处,但又有明显不同。
ThreadLocalMap 使用开放地址法(线性探测 + 扫描回环)处理哈希冲突,而不是像 HashMap 那样用链表或红黑树。它是一个固定长度的数组,冲突时会向后一个个找空位(或可复用的 stale 位置),必要时回环头部,并会定期清理 stale(已过期)项。
想要详细了解可以慢慢分析 replaceStaleEntry
方法。
相同的,就算产生了hash冲突,在 get
时,它从 hash
计算出的索引开始,向后线性探测,逐个比较 entry.get() == 当前ThreadLocal
对象,一旦找到匹配,就返回 value
。由于插入时也是按此顺序探测,因此一定能找回来。
5. 最后
看完你应该对ThreadLocal
有个全新的认识了,虽然概念很容易理解,但我们知其然更要知其所以然。通过现象看本质。