导致 JVM 内存泄露的 ThreadLocal 详解
为什么要有 ThreadLocal
当我们在学习JDBC时获取数据库连接时,每次CRUD的时候都需要再一次的获取连接对象,并把我们的sql交给连接对象实现操作。
在实际的工作中,我们不会每次执行 SQL 语句时临时去建立连接,而是会借助数据库连接池,同时因为实际业务的复杂性,为了保证数据的一致性,我们还会引入事务操作。
但是呢如果我们采用数据库连接池的话,每次获取的都不是同一个连接对象,万一我们需要执行事务机制的话,就容易造成事务失效了!!!数据库执行事务时,事务的开启和提交、语句的执行等都是必须在一个连接中的。实际上,上面的代码要保证数据的一致性,就必须要启用分布式事务。
怎么解决这个问题呢?有一个解决思路是,把数据库连接作为方法的参数,在方法之间进行传递,但是我们再写ssm或者springboot的项目时会发现根本不需要我们传递连接参数,那么接下来我们就看看spring如何帮我们实现的。其实稍微分析下 Spring 的事务管理器的代码就能发现端倪,在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 中,我们会看到如下代码![]()
看来,Spring 是使用一个 ThreadLocal 来实现“绑定连接到线程”的。因此ThreadLocal 下一个比较确切的定义了此类提供线程局部变量。这些变量与普通对应变量的不同之处在于,访问一个变量的每个线程(通过其 get 或 set 方法)都有自己独立初始化的变量副本。ThreadLocal 实例通常是希望将状态与线程(例如,用户 ID 或事务 ID)相关联的类中的私有静态字段。也就是说 ThreadLocal 为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。由此也可以看出 ThreadLocal 和 Synchonized 都用于解决多线程并发访问。可是 ThreadLocal 与 synchronized 有本质的差别。synchronized 是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问,ThreadLocal 则是副本机制。此时不论多少线程并发访问都是线程安全的。ThreadLocal 的一大应用场景就是跨方法进行参数传递,比如 Web 容器中,每个完整的请求周期会由一个线程来处理。结合 ThreadLocal 再使用 Spring 里的IOC 和 AOP,就可以很好的解决我们上面的事务的问题。只要将一个数据库连接放入 ThreadLocal 中,当前线程执行时只要有使用数据库连接的地方就从ThreadLocal 获得就行了。再比如,在微服务领域,链路跟踪中的 traceId 传递也是利用了 ThreadLocal 。
ThreadLocal 的使用
ThreadLocal 类接口很简单,只有 4 个方法,我们先来了解一下:• void set(Object value)设置当前线程的线程局部变量的值。• public Object get()该方法返回当前线程所对应的线程局部变量。• public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。• protected Object initialValue()返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get()或 set(Object) 时才执行,并且仅执行 1 次。 ThreadLocal 中的缺省实现直接返回一个 null 。
实现解析
实现分析
怎么实现 ThreadLocal,既然说让每个线程都拥有自己变量的副本,最容易的方式就是用一个 Map 将线程的副本存放起来,Map 里 key 就是每个线程的唯一性标识,比如线程 ID,value 就是副本值,实现起来也很简单:
考虑到并发安全性,对数据的存取用 synchronize 关键字加锁,但是 DougLee 在《并发编程实战》中为我们做过 性能测试
可以看到 ThreadLocal 的性能远超类似 synchronize 的锁实现 ReentrantLock , 比我们后面要学的AtomicInteger 也要快很多,即使我们把 Map 的实现更换为 Java中专为并发设计的 ConcurrentHashMap 也不太可能达到这么高的性能。怎么样设计可以让 ThreadLocal 达到这么高的性能呢?最好的办法则是让变量副本跟随着线程本身,而不是将变量副本放在一个地方保存,这样就可以在存取时避开线程之间的竞争。同时,因为每个线程所拥有的变量的副本数是不定的,有些线程可能有一个, 有些线程可能有 2 个甚至更多,则线程内部存放变量副本需要一个容器,而且容 器要支持快速存取,所以在每个线程内部都可以持有一个 Map 来支持多个变量副本,这个 Map 被称为 ThreadLocalMap 。
具体实现
上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap , ThreadLocalMap 是一个声明在 ThreadLocal 的静态内部类,然后 Thread 类中有一 个这样类型成员变量,也就是 ThreadLocalMap 实例化是在 Thread 内部,所以getMap 是直接返回 Thread 的这个成员。看下 ThreadLocal 的内部类 ThreadLocalMap 源码,这里其实是个标准的 Map 实现,内部有一个元素类型为 Entry 的数组,用以存放线程可能需要的多个副本变量。
可以看到有个 Entry 内部静态类,它继承了 WeakReference ,总之它记录了两个信息,一个是 ThreadLocal<?> 类型,一个是 Object 类型的值。 getEntry 方法则是获取某个 ThreadLocal 对应的值, set 方法就是更新或赋值相应的 ThreadLocal对应的值。回顾我们的 get 方法,其实就是拿到 每个线程独有的 ThreadLocalMap然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry ,然后就可以拿到相应的值返回出去。当然,如果 Map 为空,还会先进行 map 的创建,初始化等工作。
Hash 冲突的解决
什么是 Hash ,就是把任意长度的输入(又叫做预映射, pre-image ),通过散列算法,变换成固定长度的输出,该输出就是散列值,输入的微小变化会导致输出的巨大变化。所以 Hash 常用在消息摘要或签名上,常用 hash 消息摘要算法有:(1)MD4(2) MD5 它对输入仍以 512 位分组,其输出是 4 个 32 位字的级联(3)SHA-1 及其他。Hash 转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间, 不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。比如有 10000 个数放到 100 个桶里,不管怎么放,有个桶里数字个数一定是大于2 的。所以 Hash 简单的说就是一种将任意长度的消息压缩到某一固定长度的消息 摘要的函数。常用 HASH 函数:直接取余法、乘法取整法、平方取中法。 Java里的 HashMap 用的就是直接取余法。我们已经知道 Hash 属于压缩映射,一定能会产生多个实际值映射为一个Hash 值的情况,这就产生了冲突,常见处理 Hash 冲突方法:开放定址法:
基本思想是,出现冲突后按照一定算法查找一个空位置存放,根据算法的不同又可以分为线性探测再散列(依次向后查找)、二次探测再散列(依次向前后查找,增量为1,2,3的二次方)、伪随机探测再散列(随机产生一个增量位移)。
ThreadLocal 里用的则是线性探测再散列
链地址法:
这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。Java里的 HashMap 用的就是链地址法,为了避免 hash 洪水攻击,1.8 版本开始还引入了红黑树。
再哈希法:
这种方法是同时构造多个不同的哈希函数:Hi=RH1(key) i=1,2,…,k 当哈希地址 Hi=RH1(key)发生冲突时,再计算 Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
引发的内存泄漏分析 (应该被回收的内存没有被回收)
前提知识
Object o = new Object();这个 o ,我们可以称之为对象引用,而 new Object() 我们可以称之为在内存中产生了一个对象实例。
当写下 o=null 时,只是表示 o 不再指向堆中 object 的对象实例,不代表这个对象实例不存在了。强引用就是指在程序代码之中普遍存在的,类似“ Object obj=new Object ()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象, 在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK1.2 之后,提供了 SoftReference 类来实现软引用。弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了PhantomReference 类来实现虚引用。
内存泄漏的现象
我们启用一个线程池,大小固定为 5 个线程
场景 1 ,首先任务中不执行任何有意义的代码,当所有的任务提交执行完成后,可以看见,我们这个应用的内存占用基本上为 25M 左右
场景 2 ,然后我们只简单的在每个任务中 new 出一个5M的大小数组,执行完成后我们可以看见,内存占用基本和场景 1 同
场景 3 ,当我们启用了 ThreadLocal 以后,并且同时存入这个数组:
可以发现内存增大了很多倍,因此发生内存泄漏。
分析
根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
图中的虚线表示弱引用。这样,当把 threadlocal 变量置为 null 以后,没有任何强引用指向 threadlocal实例,所以 threadlocal 将会被 gc 回收。这样一来, ThreadLocalMap 中就会出现key 为 null 的 Entry ,就没有办法访问这些 key 为 null 的 Entry 的 value ,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value ,而这块 value 永远不会被访问到了,所以存在着内存泄露。只有当前 thread 结束以后, current thread 就不会存在栈中,强引用断开,Current Thread、 Map value 将全部被 GC 回收。最好的做法是不在需要使用ThreadLocal 变量后,都调用它的 remove() 方法,清除数据。从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?下面我们分两种情况讨论:key 使用强引用:对 ThreadLocal 对象实例的引用被置为 null 了,但是 ThreadLocalMap 还持有这个 ThreadLocal 对象实例的强引用,如果没有手动删除,ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。key 使用弱引用:对 ThreadLocal 对象实例的引用被被置为 null 了,由于ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除, ThreadLocal 的 对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set , get , remove 都 有机会被回收。比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread一样长,如果都没有手动删除对应 key ,都会导致内存泄漏,但是使用弱引用可以多一层保障。因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引 用。总结
JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
JVM 利用调用 remove 、 get 、 set 方法的时候,回收弱引用。当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove 、 get、 set 方法,那么将导致内存泄漏。使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了 value 可能造成累积的情况。
错误使用 ThreadLocal 导致线程不安全
运行后:
为什么每个线程都输出 115 ?难道他们没有独自保存自己的 Number 副本吗?为什么其他线程还是能够修改这个值?仔细考察 ThreadLocal 和 Thead 的代码,我们发现 ThreadLocalMap 中保存的其实是对象的一个引用 ,这样的话,当有其他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有的对象引用所指向的同一个对象实例。这也就是为什么上面的程序为什么会输出一样的结果。而上面的程序要正常的工作,应该的用法是让每个线程中的 ThreadLocal 都应该持有一个新的 Number 对象。(如图所示,将static变量去掉以免都指向同一个实例,或者调用ThreadLocal的初始化方法每次返回一个新的对象)![]()
相关文章:

导致 JVM 内存泄露的 ThreadLocal 详解
为什么要有 ThreadLocal 当我们在学习JDBC时获取数据库连接时,每次CRUD的时候都需要再一次的获取连接对象,并把我们的sql交给连接对象实现操作。 在实际的工作中,我们不会每次执行 SQL 语句时临时去建立连接,而是会借助数据库连接…...
使用预约小程序app有什么方便之处
一、用户人群广:无论是老年人、残疾人还是上班族,只要有需要,都可以通过该小程序轻松预约服务。例如,行动不便的老年人或残疾人,可以通过小程序约家政服务、医疗护理等服务,省去了亲自前往服务场所的麻烦。…...
【转】ubuntu 安装 OpenCv 4.6脚本 installOCV.sh
摘自 https://github.com/opencv/opencv/issues/22132 好东西,收一下。 installOCV.sh#! /bin/bash VER4.6.0 PYTHON_VERSION3.8 CORES2 echo "Script for installing the OpenCV $VER on Ubuntu 18.04 LTS" echo "Updating the OS..." sudo …...
Android 视图动画与属性动画的区别
Android的视图动画和属性动画在功能和使用上有一些明显的区别。 视图动画主要作用于视图,实现如缩放、旋转等效果。这种动画效果相对固定,只能应用于视图对象,且只能改变视图的大小和位置,而不能真正改变视图的属性。视图动画在A…...

Springboot——jxls实现同sheet多个列表展示
文章目录 前言制定模板1、限定模板数据的范围2、设定报表展示项 编写测试类1、将xls模板文件放于 resource 下的 doc文件夹中2、导入依赖文件3、编写接口和导出逻辑 效果预览结论 前言 在之前的博客中Springboot——使用jxls实现excel模板导出excel,具体说明了jxls…...

分布式软件架构——服务端缓存的三种属性
服务端缓存 在透明多级分流系统中,我们以流量从客户端中发出开始,以流量到达服务器集群中真正处理业务的节点结束。一起探索了在这个过程中与业务无关的一些通用组件,包括DNS、CDN、客户端缓存,等等。 实际上,服务端缓…...
Flink之Watermark策略代码模板
方式作用WatermarkStrategy.noWatermarks()不生成watermarkWatermarkStrategy.forMonotonousTimestamps()紧跟最大事件时间watermark生成策略WatermarkStrategy.forBoundedOutOfOrderness()允许乱序watermark生成策略WatermarkStrategy.forGenerator()自定义watermark生成策略 …...

ubuntu 安装postgresql,增加VECTOR向量数据库插件 踏坑详细流程
PGSQL安装,删除,运行,修改密码流程 Ubuntu18.04安装与配置postgresql含远程连接教程(含踩坑记录)_sudo apt-get install postgresql-CSDN博客 详细安装流程以上博客,自己也记录下 安装vector扩展连接 声明…...

基于Springboot实现影视影院订票选座管理系统【项目源码+论文说明】分享
基于Springboot实现影视影院订票选座管理系统演示 摘要 本论文主要论述了如何使用JAVA语言开发一个影城管理系统 ,本系统将严格按照软件开发流程进行各个阶段的工作,采用B/S架构,面向对象编程思想进行项目开发。在引言中,作者将论…...

mysql批量插入数据,跳过唯一索引报错
数据准备 DROP TABLE IF EXISTS user1; CREATE TABLE user1 ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(45) NULL, age INT(3) NOT NULL, PRIMARY KEY (id), UNIQUE INDEX u_name (name));insert into user1(name, age) values (zhangshan, 18), (lisi, 19);1. INSERT I…...

论文阅读--Energy efficiency in heterogeneous wireless access networks
异构无线接入网络的能源效率 论文信息:Navaratnarajah S, Saeed A, Dianati M, et al. Energy efficiency in heterogeneous wireless access networks[J]. IEEE wireless communications, 2013, 20(5): 37-43. I. ABSTRACT && INTRODUCTION 本文提出了无…...

Redis的C客户端(hiredis库)使用
文章目录 1、Ubuntu安装redis服务端2、hiredis库的安装3、同步API接口的使用3.1、连接redis数据库redisConnect3.2、发送需要执行的命令redisCommand3.3、redisCommandArgv函数3.4、redisAppendCommand*函数支持管道命令3.5、释放资源3.6、同步连接代码 3.7、异步连接4、redis连…...

光引擎、光模块、光器件之间的关系和区别
最近小编有收到一些用户问“光引擎、光模块、光器件之间的关系和区别?”,众所周知光通信技术一直在不断演进,为满足不断增长的数据传输需求提供了强大的解决方案。而光通信系统中,光引擎、光模块和光器件是关键的组成部分…...

【办公-excel】两个时间相减 (二) - 带毫秒的时间进行相减操作
一、使用内部函数 1.1 效果展示 TEXT(((RIGHT(TEXT(B2,"yyyy-mm-dd hh:mm:ss.000"),LEN(TEXT(B2,"yyyy-mm-dd hh:mm:ss.000"))-FIND(".",TEXT(B2,"yyyy-mm-dd hh:mm:ss.000")))-RIGHT(TEXT(A2,"yyyy-mm-dd hh:mm:ss.000"),…...

二次封装View Design的table组件,实现宽度自适应,内容在一行展示
由于table组件本身并不支持宽度自适应,但实际项目需要,而且多处有用到table组件,所以尝试着自己来二次封装一下组件 想法 刚开始的想法很简单,就是获取每一列中数据和标题在表格中的长度,然后将当中最大的长度作为该列…...

Node.js代码漏洞扫描工具介绍——npm audit
npm audit 运行安全检查 主要作用:检查命令将项目中配置的依赖项的描述提交到默认注册中心,并要求报告已知漏洞。如果发现任何漏洞,则将计算影响和适当的补救措施。如果 fix 提供了参数,则将对包树应用补救措施。 具体参考&#x…...
node.js知识系列(3)-每天了解一点
目录 1. Express.js 中的中间件2. 处理路由和请求3. RESTful 路由4. 身份验证和授权5. 视图引擎6. 错误处理中间件7. 文件上传处理8. Cookie 和 Session 管理9. 路由参数和查询参数10. 处理跨域请求(CORS) 👍 点赞,你的认可是我创…...

Zabbix监控系统 自定义监控项、自动发现与自动注册
Zabbix监控系统 自定义监控项、自动发现与自动注册 一、自定义监控内容部署实例二、zabbix 自动发现与自动注册部署实例2.1 部署zabbix自动发现 一、自定义监控内容部署实例 案列:自定义监控客户端服务器登录的人数 需求:限制登录人数不超过 3 个&#…...
Python信号之分享
在了解了Linux的信号基础之后,Python标准库中的signal包就很容易学习和理解。signal包负责在Python程序内部处理信号,典型的操作包括预设信号处理函数,暂停并等待信号,以及定时发出SIGALRM等。要注意,signal包主要是针…...

环信web、uniapp、微信小程序SDK报错详解---登录篇
项目场景: 记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40 (一) 登录用户报400 原因分析: 从console控制台输出及networ…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...
微服务通信安全:深入解析mTLS的原理与实践
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、引言:微服务时代的通信安全挑战 随着云原生和微服务架构的普及,服务间的通信安全成为系统设计的核心议题。传统的单体架构中&…...

Canal环境搭建并实现和ES数据同步
作者:田超凡 日期:2025年6月7日 Canal安装,启动端口11111、8082: 安装canal-deployer服务端: https://github.com/alibaba/canal/releases/1.1.7/canal.deployer-1.1.7.tar.gz cd /opt/homebrew/etc mkdir canal…...

PydanticAI快速入门示例
参考链接:https://ai.pydantic.dev/#why-use-pydanticai 示例代码 from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel from pydantic_ai.providers.openai import OpenAIProvider# 配置使用阿里云通义千问模型 model OpenAIMode…...