导致 JVM 内存泄露的 ThreadLocal 详解

news2024/11/26 2:26:37

        为什么要有 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()
        将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK
5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动
被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它
可以加快内存回收的速度。
        • protected Object initialValue()
        返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为
了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get()
set(Object) 时才执行,并且仅执行 1 次。 ThreadLocal 中的缺省实现直接返回一
null

    实现解析

        实现分析

        怎么实现 ThreadLocal,既然说让每个线程都拥有自己变量的副本,最容易的方式就是用一个 Map 将线程的副本存放起来,Map key 就是每个线程的唯一性标识,比如线程 IDvalue 就是副本值,实现起来也很简单:

        

        考虑到并发安全性,对数据的存取用 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=RH1keyi=12,…,k 当哈希地址 Hi=RH1key)发生冲突时,再计算 Hi=RH2key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

        建立公共溢出区

        这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

     引发的内存泄漏分析 (应该被回收的内存没有被回收)

        前提知识

        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的初始化方法每次返回一个新的对象)

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

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

相关文章

Android用户登录与数据存储:从权限请求到内外部存储的完整实践【完整实践步骤、外部存储、内部存储】

步骤 1: 登录页面布局 在 MainActivity 中实现用户登录功能&#xff0c;首先创建一个布局文件 activity_main.xml 包含用户名和密码的输入字段以及登录按钮。 <!-- activity_main.xml --> <LinearLayoutxmlns:android"http://schemas.android.com/apk/res/andr…

Tomcat服务器下载、安装、配置环境变量教程(超详细)

请先配置安装好Java的环境&#xff0c;若没有安装&#xff0c;请参照如下博客上的步骤进行安装&#xff01; 安装Java环境教程Windows配置Java环境变量(下载、安装、配置环境)_第三女神程忆难的博客-CSDN博客 Tomcat部署Web项目&#xff08;一&#xff09;内嵌 Tomcat部署网站…

Java基于SpringBoot的社区维修平台

文章目录 简介环境需要住户前台功能模块管理员功能模块住户后台功能模块维修员后台功能模块源码咨询 简介 系统管理也都将通过计算机进行整体智能化操作,对于社区维修平台所牵扯的管理及数据保存都是非常多的,例如住户管理、社区公告管理、维修工管理、维修订单管理、接单信息…

STM32H723加上ThreadX,时钟不准确

硬件用的晶振是8MHz 的&#xff0c;默认这里是25&#xff0c;需要改为8&#xff0c;然后主频用400MHz 其他的&#xff1a; tx_thread_sleep(1000); //延时就是1秒了

【java问题排查方法】

文章目录 一、内存泄漏排查方案 一、内存泄漏排查方案 jmap是Java JDK提供的一个命令行工具&#xff0c;用于生成Java虚拟机的堆转储快照dump文件&#xff0c;它可以帮助开发者查看Java堆的内存使用情况&#xff0c;诊断内存泄漏和其他内存问题。 要使用jmap&#xff0c;需要…

tcpdump(五)命令行参数讲解(四)

一 案例讲解 tcpdump官方参考文档 最全的tcpdump手册 强调&#xff1a; -nn 选项一般是must 必选 ① 现场分析并保留现场信息 tcpdump -l | tee dat 使用tee来把tcpdump的输出同时放到文件dat和标准输出中场景&#xff1a; 自己现场分析同时把现场信息保留下来 ② …

tcpdump(四)命令行参数讲解(三)

一 BPF高级过滤条件 高级filter官方地址 常见需求案例汇总 过滤的目的&#xff1a;获取最精细、准确的数据思考&#xff1a; 抓取更精确的包?1) tcp/ip 报文结构要精通,这样才能知道如何获取自己想要的信息 -> 偏移量2) tcpdump 的synax语法要精通,要正确写对3) 多练习…

应用超高频RFID技术的银行款箱柜资产管理系统

背景概述 随着银行后台管理的集中化思路&#xff0c;对款箱的管理需要实现“安全、高效”的“管、控、营”一体化&#xff0c;传统的人工款箱管理模式和数据采集方式已无法满足银行管理的快速、准确要求&#xff0c;严重影响了银行整体运行效率。 传统的款箱管理存在以下问题…

【管理运筹学】第 9 章 | 网络计划(1,网络图的组成及绘制)

文章目录 引言一、网络图的组成及绘制1.1 网络图的组成1. 基本要素2. 线路与关键线路3. 网络图的类型 1.2 网络图的绘制1. 画图原则2. 绘图一般步骤 写在最后 引言 大纲里关于网络计划这一章的描述&#xff0c;就两个&#xff0c;一个是基本概念&#xff1a;网络计划、时间参数…

Zabbix监控系统与部署Zabbix6.0监控(系列操作完整版)

目录 Zabbix 6.0 1 zabbix 是什么 1.1 zabbix 监控原理 1.2 Zabbix 6.0 新特性 1.3 Zabbix 6.0 功能组件 2 Zabbix 6.0 部署 2.1 部署 zabbix 服务端 2.1.1 部署 Nginx PHP 环境并测试 2.1.2 部署数据库&#xff0c;要求 MySQL 5.7 或 Mariadb 10.5 及以上版本 2.1.3…

项目_数据可视化| 折线图.散点图.随机漫步

安装matplotlib 在正式开始编写程序之前&#xff0c;需要先安装pip、matplotlib模块&#xff0c;苹果系统的安装问题在之前的文章中有相关介绍内容&#xff0c;如果pycharm运行模块报错&#xff0c;可以再次检查是否版本兼容问题。 绘制折线图 调用subplot&#xff08;&#x…

Java代码hello word

一、安装java环境 开始学习java之前&#xff0c;我们的第一步就是安装java环境&#xff0c;即常说的JDK和JRE&#xff0c;此处就不在详细介绍配置环境过程&#xff0c;可以到网上搜索java开发环境配置。 二、编写第一个程序 工具&#xff1a; 常用的java编写工具有IDE、Notep…

数据结构与算法(五):树

参考引用 Hello 算法 Github&#xff1a;hello-algo 1. 二叉树 二叉树&#xff08;binary tree&#xff09;是一种非线性数据结构&#xff0c;代表着祖先与后代之间的派生关系&#xff0c;体现着“一分为二”的分治逻辑 与链表类似&#xff0c;二叉树的基本单元是节点&#xff…

【Qt】顶层窗口和普通窗口区别以及用法

区别 在Qt项目开发中&#xff0c;经常会用到窗体控件用于显示及数据操作和其他交互等。 但&#xff0c;窗体分为顶层窗口&#xff08;Top-level Window&#xff09;和普通窗口&#xff08;Regular Window&#xff09;。 他们之间是有区别的&#xff0c;包括在项目实际中的用法…

【Vue面试题十一】、Vue组件之间的通信方式都有哪些?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;Vue组件之间的通信方式都…

学习网络编程No.7【应用层之序列化和反序列化】

引言&#xff1a; 北京时间&#xff1a;2023/9/14/19:13&#xff0c;下午刚刚更完文章&#xff0c;是一篇很久很久以前的文章&#xff0c;由于各种原因&#xff0c;留到了今天更新&#xff0c;非常惭愧呀&#xff01;目前在上学校开的一门网络课程&#xff0c;学校的课听不了一…

leetCode 1143.最长公共子序列 动态规划

1143. 最长公共子序列 - 力扣&#xff08;LeetCode&#xff09; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0 。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串…

Linux登录自动执行脚本

一、所有用户每次登录时自动执行。 1、在/etc/profile文件末尾添加。 将启动命令添加到/etc/profile文件末尾。 2、在/etc/profile.d/目录下添加sh脚本。 在/etc/profile.d/目录下新建sh脚本&#xff0c;设置每次登录自动执行脚本。有用户登录时&#xff0c;/etc/profile会遍…

一文带你读懂残差网络ResNet

&#x1f680; 作者 &#xff1a;“码上有钱” &#x1f680; 文章简介 &#xff1a;AI-残差算法 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac;简介 残差网络&#xff08;Residual Neural Network, ResNet&#xff09;是深度神经网络的一种。它通…

2.1 关系数据结构及形式化定义

思维导图&#xff1a; 2.1.1 关系 笔记&#xff1a; 关系数据库模型是一个简单但强大的方式来表示数据及其之间的关系。下面是这节的关键内容&#xff1a; - **关系模型核心概念** * 关系数据模型的核心是“关系”&#xff0c;它在逻辑上表现为一个二维表。 * 此表中&a…