ThreadLocal内部结构深度解析
目录
一、ThreadLocal
例子
内部结构分析
ThreadLocal.get()源码解析
图示详解
二、ThreadLocal.set()
源码流程图
源码解析
思考
三、ThreadLocal.remove()
流程
源码解析
四、ThreadLocal的内存泄露
延伸--Java的四种引用类型
发生内存泄露的场景
ThreadLocal是Java中一个非常重要且常用的线程局部变量工具类,它使得每个线程可以独立地持有自己的变量副本,而不是共享变量,解决了多线程环境下变量共享的线程安全问题。下面我将从多个维度深入分析ThreadLocal的内部结构和工作原理。
一、ThreadLocal
// 1. 初始化:创建ThreadLocal变量
private static ThreadLocal<T> threadLocal = new ThreadLocal<>();// 2. 设置值:为当前线程设置值
threadLocal.set(value); // value为要存储的泛型对象// 3. 获取值:获取当前线程的值
T value = threadLocal.get(); // 返回当前线程存储的值// 4. 移除值:清除当前线程的ThreadLocal变量(防止内存泄漏)
threadLocal.remove();
【注】使用时,通常将ThreadLocal声明为
static final
以保证全局唯一性private static ThreadLocal<T> threadLocal = ThreadLocal.withInitial(() -> initialValue);
【注:】 withInitial里面放的是任何能够返回 T 类型实例的 Lambda / Supplier。
只要 Supplier 的逻辑最终能 new(或从缓存、工厂、单例池等)拿出一个 T,就合法。
例子
例子中看似我是在讲add()方法,但是要知道ThreadLocal里面底层是没有add这个方法的,这只是实现的一个业务逻辑,而底层是用get()方法实现的,所以下面的源码解析,也是在解析get()方法。
package com.qcby.test;import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class ThreadLocalTest {private List<String> messages = new ArrayList<>();public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);public static void add(String message) {holder.get().messages.add(message);}public static List<String> clear() {List<String> messages = holder.get().messages;holder.remove();return messages;}public static void main(String[] args) throws InterruptedException {// 创建线程池ExecutorService executor = Executors.newFixedThreadPool(10);// 提交10个任务for (int i = 0; i < 10; i++) {final int threadId = i;executor.submit(() -> {ThreadLocalTest.add("线程" + threadId + "的消息" );// 打印当前线程的消息System.out.println("线程" + threadId + "的消息列表: " + holder.get().messages);// 清除当前线程的ThreadLocalThreadLocalTest.clear();});}// 关闭线程池executor.shutdown();executor.awaitTermination(1, TimeUnit.SECONDS);// 主线程检查自己的ThreadLocal(应该是空的)System.out.println("主线程的消息列表: " + holder.get().messages);}
}
内部结构分析
根据这里get的源码追溯分析:
追溯到:
ThreadLocal.get()源码解析
/*** 获取当前线程的ThreadLocal变量值*/
public T get() {// 1. 获取当前线程对象Thread t = Thread.currentThread();// 2. 获取当前线程的ThreadLocalMap(线程私有数据存储结构)ThreadLocalMap map = getMap(t);// 3. 如果map已存在if (map != null) {// 3.1 以当前ThreadLocal实例为key(也就是代码中的holder),获取对应的EntryThreadLocalMap.Entry e = map.getEntry(this);// 3.2 如果Entry存在if (e != null) {// 3.2.1 强转为泛型类型并返回值@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 4. 如果map不存在或未找到值,初始化并返回默认值return setInitialValue();
}/*** 获取线程的ThreadLocalMap(实际是Thread类的threadLocals字段)*/
ThreadLocalMap getMap(Thread t) {return t.threadLocals; // 直接返回线程对象的成员变量
}/*** 初始化值并存入ThreadLocalMap*/
private T setInitialValue() {// 1. 获取初始值(子类可重写initialValue()方法)T value = initialValue();// 2. 获取当前线程Thread t = Thread.currentThread();// 3. 获取线程的ThreadLocalMapThreadLocalMap map = getMap(t);// 4. 如果map已存在,直接设置值if (map != null) {map.set(this, value);} else {// 5. 如果map不存在,创建新map并存入初始值createMap(t, value);}// 6. 返回初始值return value;
}/*** 创建线程的ThreadLocalMap并存入第一个值*/
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}/*** 默认初始值实现(可被withInitial覆盖)*/
protected T initialValue() {return null; // 默认返回null
}
图示详解
所以执行结果:
可以看见一个线程中只有一个信息,而不是它们统一堆砌在一起,原因就是底层是每个线程创建了一个Map对象,每个Map的value就是存入的messages本质是对象,也就是T--ThreadLocalTest对象们,并且它们Map中的Entry中的Key值都是一样的,都是这个ThreadLocal,也就是holder。
【注】并不是每个线程的Map只能存放一个value对象,是这里我展示的例子里,一个线程只存了一条,完全可以存入很多条消息,然后add()时就会累加在Map已经创建好的Entry后面也就是:
当然既然是Map,存储Entry就涉及Hash了,这个以后再详谈。
二、ThreadLocal.set()
源码流程图
源码解析
/*** 设置当前线程的ThreadLocal变量值* @param value 要存储的值,将保存在当前线程的ThreadLocal副本中*/
public void set(T value) {// 1. 委托给内部set方法,传入当前线程和值set(Thread.currentThread(), value);// 2. 如果启用了虚拟线程追踪标志,进行堆栈转储if (TRACE_VTHREAD_LOCALS) {dumpStackIfVirtualThread();}
}/*** 设置载体线程的ThreadLocal变量值(专用于虚拟线程场景)* @param value 要存储的值,将保存在载体线程的ThreadLocal副本中*/
void setCarrierThreadLocal(T value) {// 1. 断言当前对象必须是CarrierThreadLocal类型assert this instanceof CarrierThreadLocal<T>;// 2. 委托给内部set方法,传入当前载体线程和值set(Thread.currentCarrierThread(), value);
}/*** 内部set方法,实际执行存储操作* @param t 目标线程对象* @param value 要存储的值*/
private void set(Thread t, T value) {// 1. 获取目标线程的ThreadLocalMapThreadLocalMap map = getMap(t);// 2. 如果map已存在if (map != null) {// 2.1 直接设置键值对(以当前ThreadLocal实例为key)map.set(this, value);} else {// 3. 如果map不存在,创建新map并存入初始值createMap(t, value);}
}/*** 获取线程的ThreadLocalMap(实际是Thread类的threadLocals字段)* @param t 目标线程对象* @return 线程的ThreadLocalMap对象*/
ThreadLocalMap getMap(Thread t) {return t.threadLocals; // 直接返回线程对象的成员变量
}/*** 创建线程的ThreadLocalMap并存入第一个值* @param t 目标线程对象* @param firstValue 要存储的第一个值*/
void createMap(Thread t, T firstValue) {// 创建新的ThreadLocalMap,以当前ThreadLocal为key,firstValue为值t.threadLocals = new ThreadLocalMap(this, firstValue);
}/*** 虚拟线程调试方法:如果是虚拟线程则转储堆栈*/
private void dumpStackIfVirtualThread() {// 调试用方法,当TRACE_VTHREAD_LOCALS为true时调用if (Thread.currentThread().isVirtual()) {new Exception("VirtualThreadLocal set").printStackTrace();}
}
思考
看过源码,尤其是流程图后,会感觉set和get好像十分相像,都是会先判断有没有Map,没有就初始化一个新的,然后调用get时会通过withInitial获取一个ThreadLocalTest实例,放在一个Entry中,默认key是Thread Local,然后value是ThreadLocalTest,也就是message。
那么究竟有什么区别呢?或是换个问题,为什么我们在例子中add()方法中使用get,而不是set方法呢?这就涉及到它们的核心源码了:
// get() 的逻辑(保留旧值)
if (map != null) {Entry e = map.getEntry(this);if (e != null) {return (T)e.value; // 直接返回,不修改}
}
return setInitialValue(); // 懒初始化
==========================================
// set() 的逻辑(覆盖旧值)
if (map != null) {map.set(this, value); // 强制更新
} else {createMap(t, value); // 初始化新Map
}
总结而言:
行为 | get() | set() |
---|---|---|
对已有值的处理 | 保留旧值,直接返回 | 覆盖旧值 |
初始化值的来源 | 通过 withInitial 的 Supplier | 使用传入的新值 |
典型用途 | 获取并操作线程本地变量 | 强制更新线程本地变量 |
内存泄漏风险 | 无 | 有(需手动清理旧值) |
就可以形象理解为:
操作 | 类比现实场景 | 结果 |
---|---|---|
get() | 打开抽屉,取出笔记本继续写笔记 | 笔记内容不断累积 |
set() | 扔掉旧笔记本,换一本新的空本子 | 旧笔记丢失,只能写新内容 |
三、ThreadLocal.remove()
流程
调用remove()
↓
获取当前线程的ThreadLocalMap
↓
找到对应ThreadLocal的Entry
↓
清除Entry的Key弱引用
↓
expungeStaleEntry():
1. 清理当前槽位
2. 向后探测清理过期Entry
3. 重新哈希非过期Entry
↓
减少Map的size计数
源码解析
/*** 移除当前线程的ThreadLocal变量值。此操作会:* 1. 清除当前ThreadLocal实例关联的Entry* 2. 探测式清理哈希表中其他过期的Entry* 3. 避免内存泄漏(关键方法)*/
public void remove() {// 1. 获取当前线程的ThreadLocalMap(可能为null)ThreadLocalMap m = getMap(Thread.currentThread());// 2. 如果Map存在,则移除当前ThreadLocal对应的Entryif (m != null) {m.remove(this); // this指当前ThreadLocal实例}
}/*** ThreadLocalMap内部移除Entry的核心方法* @param key 要移除的ThreadLocal实例(弱引用Key)*/
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;// 1. 计算初始哈希槽位置int i = key.threadLocalHashCode & (len-1);// 2. 线性探测查找目标Entryfor (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {if (e.get() == key) { // 匹配Key// 3. 清除Key的弱引用e.clear(); // 调用WeakReference.clear()// 4. 探测式清理过期Entry(关键!)expungeStaleEntry(i);return;}}
}/*** 探测式清理过期Entry(扩容和读取时也会触发此逻辑)* @param staleSlot 已知的过期Entry位置* @return 返回下一个可能为空的槽位索引*/
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 1. 清理当前槽位的过期Entrytab[staleSlot].value = null; // 释放Value强引用tab[staleSlot] = null; // 清空槽位size--; // 更新大小// 2. 向后遍历进行探测式清理Entry e;int i;for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {// 2.1 发现其他过期Entry,一并清理e.value = null;tab[i] = null;size--;} else {// 2.2 对非过期Entry重新哈希int h = k.threadLocalHashCode & (len - 1);if (h != i) { // 如果不在最佳位置tab[i] = null; // 清空当前位置// 2.3 将Entry移动到更接近最佳位置的空槽while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;
}/*** 清除Entry的Key引用(继承自WeakReference)*/
public void clear() {super.clear(); // 将referent(Key)置为null
}/*** 获取下一个哈希槽位置(线性探测法)* @param i 当前索引* @param len table长度* @return 下一个索引*/
private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0); // 环形探测
}
四、ThreadLocal的内存泄露
在讲解ThreadLocal的内存泄漏前,先来讲解一下内存泄漏和内存溢出:
特征 | 内存泄漏 | 内存溢出 |
---|---|---|
本质 | 内存未释放,逐渐积累 | 内存申请超过系统上限 |
过程 | 渐进式(长时间运行后暴露) | 突发性(立即或短时间内) |
结果 | 可能最终导致溢出 | 直接导致程序崩溃 |
解决方向 | 找到未释放的引用或资源 | 减少内存占用或增加系统内存 |
下图是ThreadLocal相关的内存结构图,在栈区中有threadLocal对象和当前线程对象,分别指向堆区真正存储的类对象,这俩个指向都是强引用。在堆区中当前线程肯定是只有自己的Map的信息的,而Map中又存储着一个个的Entry节点;在Entry节点中每一个Key都是ThreadLocal的实例,同时Value又指向了真正的存储的数据位置,以上便是下图的引用关系。
延伸--Java的四种引用类型
- 强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
- 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
- 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
ThreadLocal中Entry的设定:
static class Entry extends WeakReference<ThreadLocal<?>> {Object value; // 实际存储的值(强引用)Entry(ThreadLocal<?> k, Object v) {super(k); // Key 是弱引用value = v;}
}//所以可以看出,严格意义来说,key这里存储的是ThreadLocal的弱引用
键(Key):
ThreadLocal
实例(弱引用,防止内存泄漏)。值(Value):线程局部变量(强引用,需手动清理)。
发生内存泄露的场景
①ThreadLocal
被回收(没有外部强引用):例如:
ThreadLocal tl = new ThreadLocal(); tl = null;
(tl
被 GC 回收)。②线程未结束(如线程池中的线程长期存活):
ThreadLocalMap
仍然持有Entry
,但Entry
的key
(ThreadLocal
)已经是null
,而value
仍然占用内存。③未调用
remove()
:如果业务代码没有显式调用
ThreadLocal.remove()
,value
会一直存在,导致内存泄漏。