当前位置: 首页 > news >正文

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 的 keyThreadLocal)已经是 null,而 value 仍然占用内存。

③未调用 remove()

如果业务代码没有显式调用 ThreadLocal.remove()value 会一直存在,导致内存泄漏。

http://www.lryc.cn/news/588556.html

相关文章:

  • 《大数据技术原理与应用》实验报告三 熟悉HBase常用操作
  • 每天一个前端小知识 Day 31 - 前端国际化(i18n)与本地化(l10n)实战方案
  • html js express 连接数据库mysql
  • Java:继承和多态(必会知识点整理)
  • 为什么资深C++开发者大部分选vector?揭秘背后的硬核性能真相!
  • 9.服务容错:构建高可用微服务的核心防御
  • #Paper Reading# Apple Intelligence Foundation Language Models
  • 微服务初步入门
  • 量子计算新突破!阿里“太章3.0”实现512量子比特模拟(2025中国量子算力巅峰)
  • 【算法训练营Day12】二叉树part2
  • 《大数据技术原理与应用》实验报告二 熟悉常用的HDFS操作
  • 【小白量化智能体】应用5:编写通达信股票交易指标及生成QMT自动交易Python策略程序
  • UDP协议的端口161怎么检测连通性
  • 【PY32】如何使用 J-Link 和 MDK 开发调试 PY32 MCU
  • 【STM32】什么在使能寄存器或外设之前必须先打开时钟?
  • java基础-1 : 运算符
  • 使用dify生成测试用例
  • 13.计算 Python 字符串的字节大小
  • HTML 文本格式化标签
  • 工业新引擎:预测性维护在工业场景中的实战应用(流程制造业为例)
  • 具身智能零碎知识点(五):VAE中对使用KL散度的理解
  • JJ20 Final Lap演唱会纪念票根生成工具
  • HashMap的长度为什么要是2的n次幂以及HashMap的继承关系(元码解析)
  • C语言:20250714笔记
  • 文本预处理(四)
  • AI驱动编程范式革命:传统开发与智能开发的全维度对比分析
  • 【DataWhale】快乐学习大模型 | 202507,Task01笔记
  • js的局部变量和全局变量
  • Java面试总结(经典题)(Java多线程)(一)
  • kotlin学习笔记