多线程系列(七) -ThreadLocal 用法及内存泄露分析

一、简介

在 Java web 项目中,想必很多的同学对ThreadLocal这个类并不陌生,它最常用的应用场景就是用来做对象的跨层传递,避免多次传递,打破层次之间的约束。

比如下面这个HttpServletRequest参数传递的简单例子!

public class RequestLocal {/*** 线程本地变量*/private static ThreadLocal<HttpServletRequest> local = new ThreadLocal<>();/*** 存储请求对象* @param request*/public static void set(HttpServletRequest request){local.set(request);}/*** 获取请求对象* @return*/public static HttpServletRequest get(){return local.get();}/*** 移除请求对象* @return*/public static void remove(){local.remove();}
}
public class MyServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {// 存储请求对象变量RequestLocal.set(req);try {// 业务逻辑...} finally {// 请求完毕之后,移除请求对象变量RequestLocal.remove();}}
}
// 在需要的地方,通过 RequestLocal 类获取HttpServletRequest对象
HttpServletRequest request = RequestLocal.get();

看完以上示例,相信大家对ThreadLocal的使用已经有了大致的认识。

当然ThreadLocal的作用还不仅限如此,作为 Java 多线程模块的一部分,ThreadLocal也经常被一些面试官作为知识点用来提问,因此只有理解透彻了,回答才能更加游刃有余。

下面我们从ThreadLocal类的源码解析到使用方式做一次总结,如果有不正之处,请多多谅解,并欢迎批评指出。

二、源码剖析

ThreadLocal类,也经常被叫做线程本地变量,也有的叫做本地线程变量,意思其实差不多,通俗的解释:ThreadLocal作用是为变量在每个线程中创建一个副本,这样每个线程就可以访问自己内部的副本变量;同时,该变量对其他线程而言是封闭且隔离的。

字面的意思很容易理解,但是实际上ThreadLocal类的实现原理还有点复杂。

打开ThreadLocal类,它总共有 4 个public方法,内容如下!

方法描述
public void set(T value)设置当前线程变量
public T get()获取当前线程变量
public void remove()移除当前线程设置的变量
public static ThreadLocal withInitial(Supplier supplier)自定义初始化当前线程的默认值

其中使用最多的就是set()get()remove()方法,至于withInitial()方法,一般在ThreadLocal对象初始化的时候,给定一个默认值,例如下面这个例子!

// 给所有线程初始化一个变量 1
private static ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 1);

下面我们重点来剖析以上三个方法的源码,最后总结如何正确的使用。

以下源码解析均基于jdk1.8

2.1、set 方法

打开ThreadLocal类,set()方法的源码如下!

public void set(T value) {// 首先获取当前线程对象Thread t = Thread.currentThread();// 获取当前线程中的变量 ThreadLocal.ThreadLocalMapThreadLocalMap map = getMap(t);// 如果不为空,就设置值if (map != null)map.set(this, value);else// 如果为空,初始化一个ThreadLocalMap变量,其中key为当前的threadlocal变量createMap(t, value);
}

我们继续看看createMap()方法的源码,内容如下!

void createMap(Thread t, T firstValue) {// 初始化一个 ThreadLocalMap 对象,并赋予给 Thread 对象// 可以发现,其实 ThreadLocalMap 是 Thread 类的一个属性变量t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// INITIAL_CAPACITY 变量的初始值为 16table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

从上面的源码上你会发现,通过ThreadLocal类设置的变量,最终保存在每个线程自己的ThreadLocal.ThreadLocalMap对象中,其中key是当前线程的ThreadLocal变量,value就是我们设置的变量。

基于这点,可以得出一个结论:每个线程设置的变量只有自己可见,其它线程无法访问,因为这个变量是线程自己独有的属性

从上面的源码也可以看出,真正负责存储value变量的是Entry静态类,并且这个类继承了一个WeakReference类。稍有不同的是,Entry静态类中的key是一个弱引用类型对象,而value是一个强引用类型对象。这样设计的好处在于,弱引用的对象更容易被 GC 回收,当ThreadLocal对象不再被其他对象使用时,可以被垃圾回收器自动回收,避免可能的内存泄漏。关于这一点,我们在下文再详细的介绍。

最后我们再来看看map.set(this, value)这个方法的源码逻辑,内容如下!

private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;// 根据hash和位运算,计算出数组中的存储位置int i = key.threadLocalHashCode & (len-1);// 循环遍历检查计算出来的位置上是否被占用for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {// 进入循环体内,说明当前位置已经被占用了ThreadLocal<?> k = e.get();// 如果key相同,直接进行覆盖if (k == key) {e.value = value;return;}// 如果key为空,说明key被回收了,重新覆盖if (k == null) {replaceStaleEntry(key, value, i);return;}}// 当没有被占用,循环结束之后,取最后计算的空位,进行存储tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}
private static int nextIndex(int i, int len) {// 下标依次自增return ((i + 1 < len) ? i + 1 : 0);
}

从上面的源码分析可以看出,ThreadLocalMapHashMap,虽然都是键值对的方式存储数据,当在数组中存储数据的下表冲突时,存储数据的方式有很大的不同。jdk1.8种的HashMap采用的是链表法和红黑树来解决下表冲突,当

ThreadLocalMap采用的是开放寻址法来解决hash冲突,简单的说就是当hash出来的存储位置相同但key不一样时,会继续寻找下一个存储位置,直到找到空位来存储数据。

jdk1.7中的HashMap采用的是链表法来解决hash冲突,当hash出来的存储位置相同但key不一样时,会将变量通过链表的方式挂在数组节点上。

为了实现更高的读写效率,jdk1.8中的HashMap就更为复杂了,当冲突的链表长度超过 8 时,链表会转变成红黑树,在此不做过多的讲解,有兴趣的同学可以翻看关于HashMap的源码分析文章。

一路分析下来,是不是感觉set()方法还是挺复杂的,总结下来set()大致的逻辑有以下几个步骤:

  • 1.首先获取当前线程对象,检查当前线程中的ThreadLocalMap是否存在
  • 2.如果不存在,就给线程创建一个ThreadLocal.ThreadLocalMap对象
  • 3.如果存在,就设置值,存储过程中如果存在 hash 冲突时,采用开放寻址法,重新找一个空位进行存储
2.2、get 方法

了解完set()方法之后,get()方法就更容易了,get()方法的源码如下!

public T get() {// 获取当前线程对象Thread t = Thread.currentThread();// 从当前线程对象中获取 ThreadLocalMap 对象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();
}

这里我们要重点看下map.getEntry(this)这个方法,源码如下!

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];// 如果找到key,直接返回if (e != null && e.get() == key)return e;else// 如果找不到,就尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {// e指的是entry ,也就是一个弱引用ThreadLocal<?> k = e.get();// 如果找到了,就返回if (k == key)return e;if (k == null)// 如果key为null,说明已经被回收了,同时将value设置为null,以便进行回收expungeStaleEntry(i);else// 如果key不是要找的那个,那说明有hash冲突,继续找下一个entryi = nextIndex(i, len);e = tab[i];}return null;
}

从上面的源码可以看出,get()方法逻辑,总共有以下几个步骤:

  • 1.首先获取当前线程对象,从当前线程对象中获取 ThreadLocalMap 对象
  • 2.然后判断ThreadLocalMap是否存在,如果存在,就尝试去获取最终的value
  • 3.如果不存在,就重新初始化默认值,以便清理旧的value

其中expungeStaleEntry()方法是真正用于清理value值的,setInitialValue()方法也具备清理旧的value变量作用。

从上面的代码可以看出,ThreadLocal为了清楚value变量,花了不少的心思,其实本质都是为了防止ThreadLocal出现可能的内存泄漏。

2.3、remove 方法

我们再来看看remove()方法,源码如下!

public void remove() {// 获取当前线程里面的 ThreadLocalMap 对象ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)// 如果不为空,就移除m.remove(this);
}
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);// 循环遍历目标key,然后将key和value都设置为nullfor (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();// 清理value值expungeStaleEntry(i);return;}}
}

remove()方法逻辑比较简单,首先获取当前线程的ThreadLocalMap对象,然后循环遍历key,将目标key以及对应的value都设置为null

从以上的源码剖析中,可以得出一个结论:不管是set()get()还是remove(),其实都会主动清理无效的value数据,因此实际开发过程中,没有必要过于担心内存泄漏的问题。

三、为什么要用 WeakReference?

另外细心的同学可能会发现,ThreadLocal中真正负责存储keyvalue变量的是Entry静态类,并且它继承了一个WeakReference类。

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

关于WeakReference类,我们在上文只是简单的说了一下,可能有的同学不太清楚,这个再次简要的介绍一下。

了解过WeakHashMap类的同学,可能对WeakReference有印象,它表示当前对象为弱引用类型

在 Java 中,对象有四种引用类型,分别是:强引用、软引用、弱引用和虚引用,级别从高依次到低。

不同引用类型的对象,GC 回收的方式也不一样,对于强引用类型,不会被垃圾收集器回收,即使当内存不足时,另可抛异常也不会主动回收,防止程序出现异常,通常我们自定义的类,初始化的对象都是强引用类型;对于软引用类型的对象,当不存在外部强引用的时候,GC 会在内存不足的时候,进行回收;对于弱引用类型的对象,当不存在外部强引用的时候,GC 扫描到时会进行回收;对于虚引用,GC 会在任何时候都可能进行回收。

下面我们看一个简单的示例,更容易直观的了解它。

public static void main(String[] args) {Map weakHashMap = new WeakHashMap();//向weakHashMap中添加4个元素for (int i = 0; i < 3; i++) {weakHashMap.put("key-"+i, "value-"+ i);}//输出添加的元素System.out.println("数组长度:"+weakHashMap.size() + ",输出结果:" + weakHashMap);//主动触发一次GCSystem.gc();//再输出添加的元素System.out.println("数组长度:"+weakHashMap.size() + ",输出结果:" + weakHashMap);}

输出结果:

数组长度:3,输出结果:{key-2=value-2, key-1=value-1, key-0=value-0}
数组长度:3,输出结果:{}

以上存储的弱引用对象,与外部对象没有强关联,当主动调用 GC 回收器的时候,再次查询WeakHashMap里面的数据的时候,弱引用对象收回,所以内容为空。其中WeakHashMap类底层使用的数据存储对象,也是继承了WeakReference

采用WeakReference这种弱引用的方式,当不存在外部强引用的时候,就会被垃圾收集器自动回收掉,减小内存空间压力。

需要注意的是,Entry静态类中仅仅只是key被设计成弱引用类型,value依然是强引用类型。

回归正题,为什么ThreadLocalMap类中的Entry静态类中的key需要被设计成弱引用类型?

我们先看一张Entry对象的依赖图!

如上图所示,Entry持有ThreadLocal对象的引用,如果没有设置引用类型,这个引用链就全是强引用,当线程没有结束时,它持有的强引用,包括递归下去的所有强引用都不会被垃圾回收器回收;只有当线程生命周期结束时,才会被回收。

哪怕显式的设置threadLocal = null,它也无法被垃圾收集器回收,因为Entrykey存在强关联!

如果Entry中的key设置成弱引用,当threadLocal = null时,key就可以被垃圾收集器回收,进一步减少内存使用空间。

但是也仅仅只是回收key,不能回收value,如果这个线程运行时间非常长,又没有调用set()get()或者remove()方法,随着线程数的增多可能会有内存溢出的风险。

因此在实际的使用中,想要彻底回收value,使用完之后可以显式调用一下remove()方法。

四、应用介绍

通过以上的源码分析,相信大家对ThreadLocal类已经有了一些认识,它主要的作用是在线程内实现变量的传递,每个线程只能看到自己设定的变量。

我们可以看一个简单的示例!

public static void main(String[] args) {ThreadLocal threadLocal = new ThreadLocal();threadLocal.set("main");for (int i = 0; i < 5; i++) {final int j = i;new Thread(new Runnable() {@Overridepublic void run() {// 设置变量threadLocal.set(String.valueOf(j));// 获取设置的变量System.out.println("thread name:" + Thread.currentThread().getName() + ", 内容:" + threadLocal.get());}}).start();}System.out.println("thread name:" + Thread.currentThread().getName() + ", 内容:" + threadLocal.get());
}

输出结果:

thread name:Thread-0, 内容:0
thread name:Thread-1, 内容:1
thread name:Thread-2, 内容:2
thread name:Thread-3, 内容:3
thread name:main, 内容:main
thread name:Thread-4, 内容:4

从运行结果上可以很清晰的看出,每个线程只能看到自己设置的变量,其它线程不可见。

ThreadLocal可以实现线程之间的数据隔离,在实际的业务开发中,使用非常广泛,例如文章开头介绍的HttpServletRequest参数的上下文传递。

五、小结

最后我们来总结一下,ThreadLocal类经常被叫做线程本地变量,它确保每个线程的ThreadLocal变量都是各自独立的,其它线程无法访问,实现线程之间数据隔离的效果。

ThreadLocal适合在一个线程的处理流程中实现参数上下文的传递,避免同一个参数在所有的方法中传递。

使用ThreadLocal时,如果当前线程中的变量已经使用完毕并且永久不在使用,推荐手动调用移除remove()方法,可以采用try ... finally结构,并在finally中清除变量,防止存在潜在的内存溢出风险。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/7469.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PyQt 入门

Qt hello - 专注于Qt的技术分享平台 Python体系下GUI框架也多了去了&#xff0c;PyQt算是比较受欢迎的一个。如果对Qt框架熟悉&#xff0c;那掌握这套框架是很简单的。 一&#xff0c;安装 1.PyQt5 pip3 install PyQt5 2.Designer UI工具 pip3 install PyQt5-tools 3.UI…

【微磁学3D绘图工具探索】Excalibur

文章目录 概要调查报告技术名词解释主要特点 技术和算法实现他能够画出怎样酷炫的图 小结 概要 微磁学中的磁学结构同时包括二维和三维&#xff0c;想要绘制得好看&#xff0c;结果清晰&#xff0c;那么就需要一些自己写的绘图代码之外的额外渲染功能&#xff0c;尤其是对于三…

C语言写的LLM训练

特斯拉前 AI 总监、OpenAI 创始团队成员 Andrej Karpathy 用 C 代码完成了 GPT-2 大模型训练过程&#xff1a;karpathy/llm.c: LLM training in simple, raw C/CUDA (github.com) 下载源码 git clone --recursive https://github.com/karpathy/llm.c.git下载模型 从HF-Mirro…

springboot+vue+elementui实现校园互助平台大作业、毕业设计

目录 一、项目介绍 二、项目截图 管理后台 1.登录&#xff08;默认管理员账号密码均为&#xff1a;admin&#xff09; 2. 用户管理 ​编辑 3.任务管理 互助单&#xff08;学生发布&#xff09; 行政单&#xff08;教师发布&#xff09; ​编辑 审核&#xff08;退回需…

springboot 引入第三方bean

如何进行第三方bean的定义 参数进行自动装配

rancher/elemental 构建不可变IOS(一)

一、什么是elemental Elemental 是 Rancher 的一个变种&#xff0c;专注于提供一个更轻量级的 Kubernetes 发行版。它旨在提供简化的部署和管理体验&#xff0c;同时保持 Kubernetes 的灵活性和强大功能。Elemental 通常针对较小的部署场景或资源受限的环境&#xff0c;例如测…

数据结构链表

数据结构链表 链表 1&#xff09;链表的概念及结构: 链表是一种物理存储结构上非连续存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的引用链接次序实现的。 2&#xff09;实际中链表的结构非常多样&#xff0c;以下情况组合起来就有8种链表结构&#xff1a; 单向、双向…

信息系统项目管理师——项目工具

直方图散点图蒙特卡洛分析 PERT分析 因果图 控制图 帕累托图 持续改进 偏差分析 鱼骨图 工作绩效报告可以包含挣值图表和信息、趋势线和预测、储备燃尽图、缺陷直方图、合同绩效信息以及风险概述信息。可表现为仪表指示图、热点报告、信号灯图或其他形式。 干系人参与度评估矩…

服务器数据恢复—ESXi虚拟机中MySQL数据库数据恢复案例

服务器数据恢复环境&#xff1a; 某品牌EVA某型号存储中部署VMware ESXi虚拟化平台&#xff0c;数据盘&#xff08;精简模式&#xff09;快照数据盘&#xff0c;虚拟机中有mysql数据库。 服务器故障&#xff1a; 机房意外断电导致该存储中的一台VMware虚拟机无法启动&#xff0…

【iOS】方法交换(Method Swizzling)

文章目录 前言一、原理与注意用法注意要点Method Swizzing涉及的相关API 二、应用场景与实践1.统计VC加载次数并打印2.防止UI控件短时间多次激活事件3.防崩溃处理&#xff1a;数组越界问题4.防KVO崩溃 总结 前言 上文讲到了iOS的消息发送机制&#xff0c;在消息机制中我们了解…

【3dmax笔记】023:阵列工具(移动+一维+二维+三维)

文章目录 一、阵列工具二、案例演示 一、阵列工具 【阵列】命令将显示【阵列】对话框&#xff0c;使用该对话框可以基于当前选择创建对象阵列。 菜单栏&#xff1a;【工具】菜单 > 【阵列】 二、案例演示 首先&#xff0c;画一个物体&#xff0c;如茶壶&#xff0c;如下图…

鸿蒙 DevEcoStudio:组件实例(页面及组件生命周期函数)

【使用onPageshow等生命周期函数】 在entry/src/main/ets/pages路径下创建Page1.ets: import router from ohos.router Entry Component struct Page1 {State message: string Hello WorldState show: booleantrueaboutToAppear(){console.log(Page1组件创建实例)}aboutToDisa…

中国教育界的泰斗级人物颜廷利:一位在多个领域具有深远影响的学者

**中国教育界的泰斗级人物颜廷利教授是一位在多个领域都有着深远影响的学者**。 山东籍文化名人颜廷利教授是一位世界级的哲学大师&#xff0c;他在学术界拥有多项创造性的成果。他不仅是国际十大姓名学专家排行榜上的佼佼者&#xff0c;颜廷利还被评为颜氏家族十大杰出名人教育…

在Windows 11环境下,生成自签名证书

在Windows 11环境下&#xff0c;使用上述命令生成自签名证书时&#xff0c;需要注意的是Windows命令行不直接支持<(command)这样的进程替换语法。因此&#xff0c;您需要稍微调整方法来实现相同的目标。下面是分步骤的操作指南&#xff1a; ### 1. 安装OpenSSL 确保您已经…

python分析预测退休后养老金金额

欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一.前言 二.代码 三.总结 一.前言 养老金,也称为退休金或退休费,是一种主要的社会养老保险待遇。它旨在保障职工退休后的基本生活需要,根据劳动者对社会所作的贡献和所具备的享受养老保险资格或退休条件,按月或一次…

【Word】写论文,参考文献涉及的上标、尾注、脚注 怎么用

一、功能位置 二、脚注和尾注区别 1.首先脚注是一个汉语词汇&#xff0c;论文脚注就是附在论文页面的最底端&#xff0c;对某些内容加以说明&#xff0c;印在书页下端的注文。脚注和尾注是对文本的补充说明。 2.其次脚注一般位于页面的底部&#xff0c;可以作为文档某处内容的…

从招标到合作:如何筛选与企业需求匹配的6sigma咨询公司

在市场竞争激烈的环境中&#xff0c;领军企业需要不断改进和创新才能在行业中保持竞争优势。为了解决产品质量、生产流程和客户满意度等方面的挑战&#xff0c;许多企业选择与6sigma咨询公司合作&#xff0c;推动企业的全面变革和持续发展。下面是企业在选择合作伙伴时通常会经…

GPT-3

论文&#xff1a;Language Models are Few-Shot Learners&#xff08;巨无霸OpenAI GPT3 2020&#xff09; 摘要 最近的工作表明&#xff0c;通过对大量文本进行预训练&#xff0c;然后对特定任务进行微调&#xff0c;在许多NLP任务和基准方面取得了实质性进展。虽然这种方法…

【LLama】Llama3 的本地部署与lora微调(基于xturn)

系列课程代码文档&#xff08;前2节课可跳过&#xff09;&#xff1a;https://github.com/SmartFlowAI/Llama3-Tutorial 课程视频&#xff1a;https://space.bilibili.com/3546636263360696/channel/series XTuner &#xff1a;https://github.com/InternLM/xtuner/blob/main/R…

如何完美解决Outlook大文件传送问题,提升办公协作效率?

在日常工作中&#xff0c;邮件是一种常用的通信方式&#xff0c;经常用来发送各类文件&#xff0c;比如报告和文档、合同和协议、财务报表、营销资料、设计文件等。但有时文件会比较大&#xff0c;因此Outlook大文件传送时&#xff0c;会遇到附件大小受限的情况。常用的解决发送…