Java多线程与高并发专题——为何每次用完 ThreadLocal 都要调用 remove()?

什么是内存泄漏

首先,我们要知道这个事情和内存泄漏有关,所以就让我们先来看一下什么是内存泄漏。

内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。

因为通常情况下,如果一个对象不再有用,那么我们的垃圾回收器 GC,就应该把这部分内存给清理掉。这样的话,就可以让这部分内存后续重新分配到其他的地方去使用;否则,如果对象没有用,但一直不能被回收,这样的垃圾对象如果积累的越来越多,则会导致我们可用的内存越来越少,最后发生内存不够用的 OOM 错误。

下面我们来分析一下,在 ThreadLocal 中这样的内存泄漏是如何发生的。

Key 的泄漏

在上一篇在 Thread 中多个 ThreadLocal 是怎么存储的?中,我们分析了 ThreadLocal 的内部结构,知道了每一个 Thread 都有一个 ThreadLocal.ThreadLocalMap 这样的类型变量,该变量的名字叫作 threadLocals。线程在访问了ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。

我们可能会在业务代码中执行了 ThreadLocal instance = null 操作,想清理掉这个 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。

GC 在垃圾回收的时候会进行可达性分析,它会发现这个 ThreadLocal 对象依然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。

JDK 开发者考虑到了这一点,所以 ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用,代码如下所示:

        /*** 继承自 WeakReference 的静态内部类,用于表示 ThreadLocalMap 中的一个条目。* 该条目使用弱引用(WeakReference)来存储 ThreadLocal 对象作为键,* 当没有强引用指向该 ThreadLocal 对象时,它可以被垃圾回收。* 同时,该条目还存储了与该 ThreadLocal 关联的值。*/static class Entry extends WeakReference<ThreadLocal<?>> {/** 与该 ThreadLocal 关联的值。 */Object value;/*** 构造一个新的 Entry 对象。** @param k 关联的 ThreadLocal 对象,作为键。* @param v 与该 ThreadLocal 关联的值。*/Entry(ThreadLocal<?> k, Object v) {// 调用父类 WeakReference 的构造函数,将 k 作为弱引用的对象super(k);// 初始化与该 ThreadLocal 关联的值value = v;}}

可以看到,这个 Entry 是 extends WeakReference。弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止 GC。因此,这个弱引用的机制就避免了 ThreadLocal 的内存泄露问题。

这就是为什么 Entry 的 key 要使用弱引用的原因。

Value 的泄漏

可是,如果我们继续研究的话会发现,虽然 ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用,还是刚才那段代码:

可以看到,value = v 这行代码就代表了强引用的发生。

正常情况下,当线程终止,key 所对应的 value 是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,如果线程迟迟不会终止,那么可能 ThreadLocal 以及它所对应的 value 早就不再有用了。在这种情况下,我们应该保证它们都能够被正常的回收。

为了更好地分析这个问题,我们用下面这张图来看一下具体的引用链路(实线代表强引用,虚线代表弱引用)

可以看到,左侧是引用栈,栈里面有一个 ThreadLocal 的引用和一个线程的引用,右侧是我们的堆,在堆中是对象的实例。

我们重点看一下下面这条链路:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value→ 可能泄漏的value实例。

这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个 Value 就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个 Value 了,此时也就发生了内存泄漏问题。

JDK 同样也考虑到了这个问题,在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。

但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value的内存泄漏。

ThreadLocal 中弱引用持有变量的具体原理是什么?

ThreadLocal 中的弱引用持有变量是一种较为复杂的机制,其主要目的是在特定情况下管理对象的生命周期,同时避免内存泄漏问题。以下是关于 ThreadLocal 中弱引用持有变量具体原理的详细阐述。

  1. ThreadLocal 的基本概念
    ThreadLocal 为每个使用该变量的线程提供独立的变量副本,这意味着不同线程可以独立地修改和访问自己的变量副本,而不会相互干扰。它在多线程编程中非常有用,可以简化线程间的数据传递和共享,同时避免了线程安全问题。
  2. 弱引用的概念及作用
    在 Java 中,弱引用是一种比软引用更弱的引用类型。弱引用不会阻止对象被垃圾回收器回收。当垃圾回收器进行垃圾回收时,如果发现某个对象只有弱引用指向它,那么这个对象就会被回收。在 ThreadLocal 中使用弱引用持有变量的主要作用是为了解决内存泄漏问题。如果 ThreadLocal 直接强引用变量,而线程一直不结束,那么这个变量就可能永远不会被回收,即使没有其他地方再使用它。而使用弱引用,当没有其他强引用指向变量时,垃圾回收器就可以回收这个变量,从而避免了内存泄漏。
  3. ThreadLocal 中弱引用持有变量的具体过程
    1. 当使用 ThreadLocal 存储变量时,ThreadLocal 会将变量包装在一个弱引用对象中,并将这个弱引用对象存储在当前线程的 ThreadLocalMap 中。
    2. ThreadLocalMap 是每个线程内部的一个数据结构,用于存储该线程中所有的 ThreadLocal 对象及其对应的变量副本。
    3. 当垃圾回收器进行垃圾回收时,如果发现某个 ThreadLocal 对象只有弱引用指向它,那么这个 ThreadLocal 对象就会被回收。此时,如果没有其他强引用指向对应的变量副本,那么这个变量副本也会被回收。
    4. 如果某个线程一直不结束,而其中的 ThreadLocal 对象已经被回收,但是对应的变量副本还没有被回收,那么就可能会出现内存泄漏问题。为了避免这种情况,在使用 ThreadLocal 时,应该在不再需要变量副本时及时调用 ThreadLocal 的 remove 方法,手动删除对应的变量副本。

例如,在一些长时间运行的服务器应用中,如果不及时清理不再使用的 ThreadLocal 变量副本,就可能会导致内存占用不断增加,最终影响系统性能。

总的来说,ThreadLocal 中弱引用持有变量的原理是通过将变量包装在弱引用对象中,利用弱引用的特性,在没有其他强引用指向变量时,允许垃圾回收器回收变量,从而避免内存泄漏问题。同时,为了确保系统的稳定性和性能,在使用 ThreadLocal 时,应该注意及时清理不再使用的变量副本。

如何避免内存泄露

分析完这个问题之后,该如何解决呢?解决方法就是我们本课时的标题:调用 ThreadLocal 的 remove方法。调用这个方法就可以删除对应的 value 对象,可以避免内存泄漏。

我们来看一下 remove 方法的源码注释:

Removes the current thread's value for this thread-local variable. If this thread-local variable is subsequently read by the current thread, its value will be reinitialized by invoking its initialValue method, unless its value is set by the current thread in the interim. This may result in multiple invocations of the initialValue method in the current thread.

翻译:

移除当前线程对此线程局部变量的值。如果当前线程随后读取此线程局部变量,其值将通过调用 `initialValue` 方法重新初始化,除非在此期间当前线程设置了其值。这可能会导致在当前线程中多次调用 `initialValue` 方法。

对应源码代码:

    /*** 移除当前线程中该线程局部变量的值。* 当后续当前线程再次调用 {@link #get} 方法读取此线程局部变量时,* 其值将通过调用 {@link #initialValue} 方法重新初始化,* 除非在此期间该线程已经调用了 {@link #set} 方法设置了新值。* 这可能会导致在当前线程中多次调用 {@code initialValue} 方法。** @since 1.5*/public void remove() {// 获取当前线程关联的 ThreadLocalMap 对象ThreadLocalMap m = getMap(Thread.currentThread());// 若当前线程存在 ThreadLocalMap 对象if (m != null)// 从 ThreadLocalMap 中移除当前 ThreadLocal 对应的条目m.remove(this);}

可以看出,它是先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法。这里的remove 方法可以把 key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了。

可以看到 remove() 的核心作用是切断 Value 的强引用链,配合线程池场景下的资源管理,避免内存泄漏和数据污染。尽管 JDK 提供了弱引用和自动清理机制,但显式调用 remove() 是保障线程安全与内存效率的必要措施,所以在使用完了 ThreadLocal 之后,我们应该手动去调用它的 remove 方法,防止内存泄漏的发生。

通常的做法是在 try-finally 块中执行 remove(),确保即使发生异常也能清理资源。

        try {// 将对象 value 设置到当前线程的 ThreadLocal 实例中threadLocal.set(value);// 业务逻辑} finally {// 确保无论业务逻辑是否抛出异常,都从当前线程的 ThreadLocal 实例中移除对象// 避免内存泄漏threadLocal.remove();}

在多线程环境下,除了调用 remove () 方法,还有哪些方式可以避免内存泄漏?

在多线程环境下,内存泄漏是一个需要高度重视的问题,因为它可能会导致系统性能下降甚至崩溃。除了调用remove()方法外,还有以下几种方式可以避免内存泄漏。

  1. 及时释放资源
    在多线程编程中,资源的及时释放是避免内存泄漏的关键。当一个线程不再需要使用某个资源时,应该尽快将其释放,以便其他线程可以使用或者系统可以回收该资源。例如,在使用文件描述符、数据库连接等资源时,应该在使用完毕后及时关闭它们。这样可以避免资源被长时间占用,从而减少内存泄漏的风险。以文件描述符为例,在多线程环境下,如果一个线程打开了一个文件,但在使用完毕后没有及时关闭该文件描述符,那么其他线程可能无法使用该文件,同时系统也无法回收该资源,从而导致内存泄漏。为了避免这种情况,可以在使用文件描述符的线程中,在文件使用完毕后立即调用close()方法关闭文件描述符。
  2. 使用对象池技术
    对象池技术是一种有效的资源管理技术,可以在多线程环境下避免内存泄漏。对象池是一组预先创建好的对象,当一个线程需要使用某个对象时,可以从对象池中获取一个对象,使用完毕后再将其放回对象池中,以便其他线程可以使用。这样可以避免频繁地创建和销毁对象,从而减少内存泄漏的风险。例如,在数据库连接池中,预先创建了一组数据库连接,当一个线程需要使用数据库连接时,可以从连接池中获取一个连接,使用完毕后再将其放回连接池中。这样可以避免频繁地创建和销毁数据库连接,从而减少内存泄漏的风险。
  3. 避免循环引用
    在多线程环境下,循环引用是导致内存泄漏的一个常见原因。循环引用是指两个或多个对象相互引用,从而导致它们的引用计数无法降为零,无法被垃圾回收器回收。为了避免循环引用,可以使用弱引用或者软引用来代替强引用。例如,在 Java 中,可以使用WeakReference或者SoftReference来代替StrongReference。当一个对象只有弱引用或者软引用指向它时,垃圾回收器可以回收该对象,从而避免内存泄漏。
  4. 使用内存泄漏检测工具
    在多线程环境下,使用内存泄漏检测工具可以帮助我们及时发现内存泄漏问题,并采取相应的措施进行修复。内存泄漏检测工具可以在程序运行时监测内存的使用情况,当发现内存泄漏时,可以给出相应的提示信息,帮助我们定位内存泄漏的位置。

总之,在多线程环境下,避免内存泄漏是一个非常重要的问题。除了调用remove()方法外,可以从资源管理、代码设计、监控机制多维度入手:

  • ​优先使用弱引用和对象池减少内存占用;
  • ​合理配置线程池并及时释放资源,避免线程复用导致数据残留;
  • ​结合代码审查与工具监控,主动发现潜在泄漏点。

总结

梳理了调用 remove () 方法最核心的原因,我们再基于生产实践总结一下,为何每次用完 ThreadLocal 都要调用 remove()?

  1. 避免内存泄漏:
    ThreadLocal 的设计是为了让每个线程都有自己独立的变量副本。当线程结束时,理论上线程相关的资源应该被回收。然而,如果在使用 ThreadLocal 后没有调用 remove () 方法,并且线程在后续的执行过程中被重用,那么旧的变量副本可能会一直存在于内存中。随着时间的推移,如果有大量的线程被创建和重用,而没有清理 ThreadLocal 中的值,就可能导致内存泄漏。因为 ThreadLocal 本身只是一个弱引用持有变量,如果没有外部强引用指向变量,并且没有调用 remove () 方法,垃圾回收器可能无法回收这个变量,从而造成内存泄漏。
  2. 数据一致性问题:
    如果不调用 remove () 方法,并且线程被重用,那么新的业务逻辑可能会读取到旧的、不相关的值。这可能会导致程序出现不可预期的行为,影响数据的一致性。例如,在一个多线程的 Web 应用中,一个线程处理完一个用户的请求后,如果没有清理 ThreadLocal 中的值,那么当这个线程被分配去处理另一个用户的请求时,可能会读取到上一个用户的相关数据,从而导致数据混淆。
  3. 提高资源利用率:
    及时清理 ThreadLocal 中的值可以释放不必要的内存占用,提高系统的资源利用率。特别是在长时间运行的服务器应用中,资源的有效管理至关重要。
    如果不清理,随着系统的运行,内存中的无用数据会越来越多,可能会影响系统的性能,甚至导致内存溢出等严重问题。

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

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

相关文章

视频推拉流EasyDSS点播平台云端录像播放异常的问题排查与解决

视频推拉流EasyDSS视频直播点播平台可提供一站式的视频转码、点播、直播、视频推拉流、播放H.265视频等服务&#xff0c;搭配RTMP高清摄像头使用&#xff0c;可将无人机设备的实时流推送到平台上&#xff0c;实现无人机视频推流直播、巡检等应用。 有用户反馈&#xff0c;项目现…

《笔记》Android 获取第三方应用及查看应用信息、apk大小、缓存、存储,以及第三方清除缓存

获取应用相关信息&#xff1a; PS:manifest标签中设置以下属性表示系统应用 android:process"system" android:sharedUserId"android.uid.system" //获取所有应用&#xff08;非系统apk&#xff0c;有些应用获取不到&#xff09; List<ApplicationInf…

【保姆级教程】Windows系统+ollama+Docker+Anythingllm部署deepseek本地知识库问答大模型,可局域网多用户访问

目录 1.Ollama 本地化部署 DeepSeek R1 1.1下载Ollama 1.2安装Ollama 1.3安装DeepSeek R1大模型 2.系统环境配置 2.1开启系统功能 2.2安装wsl 3.安装 Docker Desktop并拉取Anythingllm镜像 3.1从 Docker 官网 下载并安装。 3.2拉取镜像 3.3运行 Docker 命令 4.anyth…

Sensodrive机器人力控关节模组SensoJoint在海洋垃圾清理机器人中的拓展应用

海洋污染已成为全球性的环境挑战&#xff0c;其中海底垃圾的清理尤为困难。据研究&#xff0c;海洋中约有2600万至6600万吨垃圾&#xff0c;超过90%沉积在海底。传统上&#xff0c;潜水员收集海底垃圾不仅成本高昂&#xff0c;而且充满风险。为解决这一问题&#xff0c;欧盟资助…

【redis】AOF 的基本工作机制,顺序写入,文件同步,重写机制

RDB 最大的问题&#xff0c;就是不能实时的持久化保存数据&#xff0c;在两次生成快照之间&#xff0c;实时的数据可能会随着重启而丢失 基本工作机制 AOF&#xff1a;append only file&#xff0c;类似于 MySQL 的 binlog&#xff0c;会把每个用户的每个操作&#xff0c;都记…

【C++】动态规划从入门到精通

一、动态规划基础概念详解 什么是动态规划 动态规划&#xff08;Dynamic Programming&#xff0c;DP&#xff09;是一种通过将复杂问题分解为重叠子问题&#xff0c;并存储子问题解以避免重复计算的优化算法。它适用于具有以下两个关键性质的问题&#xff1a; 最优子结构&…

数据可视化(matplotlib)-------辅助图标的设置

目录 一、认识图表常用的辅助元素 坐标轴 二、设置坐标轴的标签、刻度范围和刻度标签 &#xff08;一&#xff09;、设置坐标轴的标签 1、xlabel()------设置x轴标签 2、ylabel()------设置y轴标签 &#xff08;二) 、设置刻度范围和刻度标签 1、xlim()和ylim()函数分别可…

CSS 用于图片的样式属性

CSS 设置图像样式 CSS中用于图片的样式属性主要包括以下几个方面&#xff1a; ‌边框和背景‌&#xff1a; ‌border‌&#xff1a;可以设置图片的边框样式、宽度和颜色。例如&#xff0c;img { border: 1px solid #ddd; } 会给图片添加1像素的实线边框&#xff0c;颜色为灰色…

Redis解决缓存击穿问题——两种方法

目录 引言 解决办法 互斥锁&#xff08;强一致&#xff0c;性能差&#xff09; 逻辑过期&#xff08;高可用&#xff0c;性能优&#xff09; 设计逻辑过期时间 引言 缓存击穿&#xff1a;给某一个key设置了过期时间&#xff0c;当key过期的时候&#xff0c;恰好这个时间点对…

Object 转 JSONObject 并排除null和““字符串

public static JSONObject objToJSONObject(Object obj) throws Exception{//创建一个 HashMap 对象 map&#xff0c;用于存储对象的属性名和属性值。//key 是属性名&#xff08;String 类型&#xff09;&#xff0c;value 是属性值&#xff08;Object 类型&#xff09;Map<…

python实现接口自动化

代码实现自动化相关理论 代码编写脚本和工具实现脚本区别是啥? 代码&#xff1a; 优点&#xff1a;代码灵活方便缺点&#xff1a;学习成本高 工具&#xff1a; 优点&#xff1a;易上手缺点&#xff1a;灵活度低&#xff0c;有局限性。 总结&#xff1a; 功能脚本&#xff1a;工…

C++特性——RAII、智能指针

RAII 就像new一个需要delete&#xff0c;fopen之后需要fclose&#xff0c;但这样会有隐形问题&#xff08;忘记释放&#xff09;。RAII即用对象把这个过程给包起来&#xff0c;对象构造的时候&#xff0c;new或者fopen&#xff0c;析构的时候delete. 为什么需要智能指针 对于…

算法系列——有监督学习——4.支持向量机

一、概述 支持向量机&#xff08;Support Vector Machine&#xff0c;SVM&#xff09;是一种应用范围非常广泛的算法&#xff0c;既可以用于分类&#xff0c;也可以用于回归。 本文将介绍如何将线性支持向量机应用于二元分类问题&#xff0c;以间隔&#xff08;margin&#x…

网络安全之前端学习(HTML篇)

前言&#xff1a;网络安全中有一个漏洞叫xss漏洞&#xff0c;就是利用网页引发弹窗&#xff0c;这就要求我们看得懂源码&#xff0c;所以我会持续更新前端学习&#xff0c;可以不精通&#xff0c;但是一定要会&#xff0c;主要掌握HTML&#xff0c;css&#xff0c;js这三项技术…

大华HTTP协议在智联视频超融合平台中的接入方法

一. 大华HTTP协议介绍 大华HTTP协议是大华股份&#xff08;Dahua Technology&#xff09;为其安防监控设备开发的一套基于HTTP/HTTPS的通信协议&#xff0c;主要用于设备与客户端&#xff08;如PC、手机、服务器&#xff09;之间的数据交互。该协议支持设备管理、视频流获取、…

介绍一下TiDB、RocksDb、levelDB、LSM 树、SSTable。

LSM 树&#xff08;Log-Structured Merge-Tree&#xff09; 核心原理&#xff1a;通过将随机写转换为顺序写优化写入性能&#xff0c;适用于写密集型场景。数据首先写入内存中的 MemTable&#xff08;有序结构&#xff0c;如跳表&#xff09;&#xff0c;当达到阈值后转为 Imm…

Powershell WSL导出导入ubuntu22.04.5子系统

导出Linux子系统 导出位置在C盘下,根据自己的实际情况更改即可Write-Host "export ubuntu22.04.5" -ForegroundColor Green wsl --export Ubuntu-22.04 c:\Ubuntu-22.04.tar 导入Linux子系统 好处是目录可用在任意磁盘路径,便于迁移不同的设备之间Write-Host &quo…

Jmeter

Jmeter 官网地址 https://jmeter.apache.org/ 下载安装包 系统环境变量 变量值JMETER_HOMEG:\Application\apache-jmeter-5.6.3 修改成自己的安装目录CLASSPATH;%JMETER_HOME%\lib\ext\ApacheJMeter_core.jar; %JMETER_HOME%\lib\jorphan.jar;Path%JMETER_HOME%\bin 中文界…

碰一碰发视频saas系统技术源头一站式开发文档

碰一碰发视频系统技术源头一站式开发文档 一、引言 在数字化信息传播高速发展的当下&#xff0c;如何让视频分享更便捷、高效&#xff0c;成为商家和开发者们关注的焦点。“碰一碰发视频”系统以其独特的交互方式和强大的功能优势&#xff0c;为视频分享领域带来了革命性变革。…

Execution failed for task ‘:path_provider_android:compileDebugJavaWithJavac‘.

What went wrong: Execution failed for task ‘:path_provider_android:compileDebugJavaWithJavac’. Could not resolve all files for configuration ‘:path_provider_android:androidJdkImage’. Failed to transform core-for-system-modules.jar to match attributes {…