阿里二面:谈谈ThreadLocal的内存泄漏问题?问麻了。。。。

引言

ThreadLocal在Java多线程编程中扮演着重要的角色,它提供了一种线程局部存储机制,允许每个线程拥有独立的变量副本,从而有效地避免了线程间的数据共享冲突。ThreadLocal的主要用途在于,当需要为每个线程维护一个独立的上下文变量时,比如每个线程的事务ID、用户登录信息、数据库连接等,可以减少对同步机制如synchronized关键字或Lock类的依赖,提高系统的执行效率和简化代码逻辑。

但是我们在使用ThreadLocal时,经常因为使用不当导致内存泄漏。此时就需要我们去探究一下ThreadLocal在哪些场景下会出现内存泄露?哪些场景下不会出现内存泄露?出现内存泄露的根本原因又是什么呢?如何避免内存泄露?

ThreadLocal原理

ThreadLocal的实现基于每个线程内部维护的一个ThreadLocalMap

public class Thread implements Runnable {
	 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMapThreadLocal类的一个静态内部类,ThreadLocal本身不能存储数据,它在作用上更像一个工具类,ThreadLocal类提供了set(T value)get()等方法来操作ThreadLocalMap存储数据。

public class ThreadLocal<T> {
    // ...
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    public T get() {
        Thread t = Thread.currentThread();
        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();
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    // ...
}

ThreadLocalMap内部维护了一个Entry数据,用来存储数据,Entry继承了WeakReference,所以Entry的key是一个弱引用,可以被GC回收。Entry数组中的每一个元素都是一个Entry对象。每个Entry对象中存储着一个ThreadLocal对象与其对应的value值。

static class ThreadLocalMap {

	static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

关于弱引用的知识点,请参考:美团一面:说一说Java中的四种引用类型?

Entry数组中Entry对象的下标位置是通过ThreadLocalthreadLocalHashCode计算出来的。

private ThreadLocalMap(ThreadLocalMap parentMap) {
	Entry[] parentTable = parentMap.table;
	int len = parentTable.length;
	setThreshold(len);
	table = new Entry[len];

	for (Entry e : parentTable) {
		if (e != null) {
			@SuppressWarnings("unchecked")
			ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
			if (key != null) {
				Object value = key.childValue(e.value);
				Entry c = new Entry(key, value);
				// 通过key的threadLocalHashCode计算下标,这个key就是ThreadLocall对象
				int h = key.threadLocalHashCode & (len - 1);
				while (table[h] != null)
					h = nextIndex(h, len);
				table[h] = c;
				size++;
			}
		}
	}
}

而从Entry数组中获取对应key即ThreadLocal对应的value值时,也是通过key的threadLocalHashCode计算下标,从而可以快速的返回对应的Entry对象。

private Entry getEntry(ThreadLocal<?> key) {
// 通过key的threadLocalHashCode计算下标,这个key就是ThreadLocall对象
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
	if (e != null && e.get() == key)
		return e;
	else
		return getEntryAfterMiss(key, i, e);
}

Thread中,可以存储多个ThreadLocal对象。ThreadThreadLocalThreadLocalMap以及Entry数组的关系如下图:

image.png

ThreadLocal在哪些场景下不会出现内存泄露?

当一个对象失去所有强引用,或者它仅被弱引用、软引用、虚引用关联时,垃圾收集器(GC)通常都能识别并回收这些对象,从而避免内存泄漏的发生。当我们在手动创建线程时,若将变量存储到ThreadLocal中,那么在Thread线程正常运行的过程中,它会维持对内部ThreadLocalMap实例的引用。只要该Thread线程持续执行任务,这种引用关系将持续存在,确保ThreadLocalMap实例及其中存储的变量不会因无引用而被GC回收。

image.png

当线程执行完任务并正常退出后,线程与内部ThreadLocalMap实例之间的强引用关系随之断开,这意味着线程不再持有ThreadLocalMap的引用。在这种情况下,失去强引用的ThreadLocalMap对象将符合垃圾收集器(GC)的回收条件,进而被自动回收。与此同时,鉴于ThreadLocalMap内部的键(ThreadLocal对象)是弱引用,一旦ThreadLocalMap被回收,若此时没有其他强引用指向这些ThreadLocal对象,它们也将被GC一并回收。因此,在线程结束其生命周期后,与之相关的ThreadLocalMap及其包含的ThreadLocal对象理论上都能够被正确清理,避免了内存泄漏问题。

实际应用中还需关注ThreadLocalMap中存储的值(非键)是否为强引用类型,因为即便键(ThreadLocal对象)被回收,如果值是强引用且没有其他途径释放,仍可能导致内存泄漏。

ThreadLocal在哪些场景下会出现内存泄露?

在实际项目开发中,如果为每个任务都手动创建线程,这是一件很耗费资源的方式,并且在阿里巴巴的开发规范中也提到,不推荐使用手动创建线程,推荐使用线程池来执行相对应的任务。那么当我们使用线程池时,线程池中的线程跟ThrealLocalMap的引用关系如下:

image.png

在使用线程池处理任务时,每一个线程都会关联一个独立的ThreadLocalMap对象,用于存储线程本地变量。由于线程池中的核心线程在完成任务后不会被销毁,而是保持活动状态等待接收新的任务,这意味着核心线程与其内部持有的ThreadLocalMap对象之间始终保持着强引用关系。因此,只要核心线程存活,其所对应的ThreadLocal对象和ThreadLocalMap不会被垃圾收集器(GC)自动回收,此时就会存在内存泄露的风险。

关于Java中的线程池参数以及原理,请参考:Java线程池最全讲解

出现内存泄露的根本原因

由上述ThreadLocalMap的结构图以及ThreadLocalMap的源码中,我们知道ThreadLocalMap中包含一个Entry数组,而Entry数组中的每一个元素就是Entry对象,Entry对象中存储的Key就是ThreadLocal对象,而value就是要存储的数据。其中,Entry对象中的Key属于弱引用。

static class ThreadLocalMap {

	static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

而对于弱引用WeakReference,在引用的对象使用完毕之后,即使内存足够,GC也会对其进行回收。

关于弱引用的知识点,请参考:美团一面:说一说Java中的四种引用类型?

image.png

Entry对象中的Key被GC自动回收后,对应的ThreadLocal被GC回收掉了,变成了null,但是ThreadLocal对应的value值依然被Entry引用,不能被GC自动回收。这样就造成了内存泄漏的风险。
image.png

在线程池环境下使用ThreadLocal存储数据时,内存泄露的风险主要源自于线程生命周期管理及ThreadLocalMap内部结构的设计。由于线程池中的核心线程在完成任务后会复用,每个线程都会维持对各自关联的ThreadLocalMap对象的强引用,这确保了只要线程持续存在,其对应的ThreadLocalMap就无法被垃圾收集器(GC)自动回收。

进一步分析,ThreadLocalMap内部采用一个Entry数组来保存键值对,其中每个条目的Key是当前线程中对应ThreadLocal实例的弱引用,这意味着当外部不再持有该ThreadLocal实例的强引用时,Key部分能够被GC正常回收。然而,关键在于Entry的Value部分,它直接或间接地持有着强引用的对象,即使Key因为弱引用特性被回收,但Value所引用的数据却不会随之释放,除非明确移除或者整个ThreadLocalMap随着线程结束而失效。

所以,在线程池中,如果未正确清理不再使用的ThreadLocal变量,其所持有的强引用数据将在多个任务执行过程中逐渐积累并驻留在线程的ThreadLocalMap中,从而导致潜在的内存泄露风险。

ThreadLocal如何避免内存泄漏

经过上述ThreadLocal原理以及发生内存泄漏的分析,我们知道防止内存泄漏,我们一定要在完成线程内的任务后,调用ThreadLocalremove()方法来清除当前线程中ThreadLocal所对应的值。其remove方法源码如下:

 public void remove() {
	 ThreadLocalMap m = getMap(Thread.currentThread());
	 if (m != null) {
		 m.remove(this);
	 }
 }

remove()方法中,首先根据当前线程获取ThreadLocalMap类型的对象,如果不为空,则直接调用该对象的有参remove()方法移除value的值。ThreadLocalMapremove方法源码如下:

private void remove(ThreadLocal<?> key) {
	Entry[] tab = table;
	int len = tab.length;
	int i = key.threadLocalHashCode & (len-1);
	for (Entry e = tab[i];
		 e != null;
		 e = tab[i = nextIndex(i, len)]) {
		if (e.get() == key) {
			e.clear();
			expungeStaleEntry(i);
			return;
		}
	}
}

由上述ThreadLocalMap中的set()方法知道ThreadLocalEntry下标是通过计算ThreadLocalhashCode获得了,而remove()方法要找到需要移除value所在Entry数组中的下标时,也时通过当前ThreadLocal对象的hashCode获的,然后找到它的下标之后,调用expungeStaleEntry将其value也置为null。我们继续看一下expungeStaleEntry方法的源码:

private int expungeStaleEntry(int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;

	// expunge entry at staleSlot
	tab[staleSlot].value = null;
	tab[staleSlot] = null;
	size--;

	// Rehash until we encounter null
	Entry e;
	int i;
	for (i = nextIndex(staleSlot, len);
		 (e = tab[i]) != null;
		 i = nextIndex(i, len)) {
		ThreadLocal<?> k = e.get();
		if (k == null) {
			e.value = null;
			tab[i] = null;
			size--;
		} else {
			int h = k.threadLocalHashCode & (len - 1);
			if (h != i) {
				tab[i] = null;

				// Unlike Knuth 6.4 Algorithm R, we must scan until
				// null because multiple entries could have been stale.
				while (tab[h] != null)
					h = nextIndex(h, len);
				tab[h] = e;
			}
		}
	}
	return i;
}

expungeStaleEntry()方法中,会将ThreadLocal为null对应的value设置为null,同时会把对应的Entry对象也设置为null,并且会将所有ThreadLocal对应的value为null的Entry对象设置为null,这样就去除了强引用,便于后续的GC进行自动垃圾回收,也就避免了内存泄露的问题。即调用完remove方法之后,ThreadLocalMap的结构图如下:

image.png

ThreadLocal中,不仅仅是remove()方法会调用expungeStaleEntry()方法,在set()方法和get()方法中也可能会调用expungeStaleEntry()方法来清理数据。这种设计确保了即使没有显式调用remove()方法,系统也会在必要时自动清理不再使用的ThreadLocal变量占用的内存资源。

需要我们特别注意的是,尽管ThreadLocal提供了remove这种机制来防止内存泄漏,但它并不会自动执行相关的清理操作。所以为了确保资源有效释放并避免潜在的内存泄露问题,我们应当在完成对ThreadLocal对象中数据的使用后,及时调用其remove()方法。我们最好(也是必须)是在try-finally代码块结构中,在finally块中明确地执行remove()方法,这样即使在处理过程中抛出异常,也能确保ThreadLocal关联的数据被清除,从而有利于GC回收不再使用的内存空间,避免内存泄漏。

总结

本文探讨了ThreadLocal的工作原理以及其内存泄漏问题及解决策略。ThreadLocal通过为每个线程提供独立的变量副本,实现多线程环境下的数据隔离。其内部通过ThreadLocalMap与当前线程绑定,利用弱引用管理键值对。但是,如果未及时清理不再使用的ThreadLocal变量,可能导致内存泄漏,尤其是在线程池场景下。解决办法包括在完成任务后调用remove方法移除无用数据。正确理解和使用ThreadLocal能够有效提升并发编程效率,但务必关注潜在的内存泄漏风险。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Python3的数据结构与算法 - 20 AVL的旋转

一、二叉搜索树的效率 平均情况下&#xff0c;二叉搜索树进行搜索的时间复杂度为O(lgn)。最坏情况下&#xff0c;二叉搜索树可能非常偏斜。&#xff08;如下图所示&#xff09;解决方法&#xff1a; 随机化插入AVL树 二、AVL树 AVL树是一棵自平衡的二叉树AVL树具有以下性质&…

自动驾驶感知新范式——BEV感知经典论文总结和对比(一)

自动驾驶感知新范式——BEV感知经典论文总结和对比&#xff08;一&#xff09; 博主之前的博客大多围绕自动驾驶视觉感知中的视觉深度估计&#xff08;depth estimation&#xff09;展开&#xff0c;包括单目针孔、单目鱼眼、环视针孔、环视鱼眼等&#xff0c;目标是只依赖于视…

YOLOv8:Roboflow公开数据集训练模型

Roboflow公开数据集 Roboflow是一个提供计算机视觉数据集管理和处理工具的平台。虽然Roboflow本身并不创建或策划公开数据集&#xff0c;但它提供了一系列功能&#xff0c;帮助用户组织、预处理、增强和导出计算机视觉数据集。 官方网站&#xff1a;https://universe.roboflow…

【Leetcode每日一题】 动态规划 - 使用最小花费爬楼梯(难度⭐)(41)

1. 题目解析 题目链接&#xff1a;746. 使用最小花费爬楼梯 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 一、设定状态表 为了解决这个问题&#xff0c;我们首先要明确一个“状态表”。这个状态表其实就是一个记录…

【蓝桥杯知识点】二分查找(超超超详细,再也不会错啦)

考完了计算机三级&#xff0c;蓝桥杯和数学建模的学习也要恢复常态啦&#xff01;今天&#xff0c;我们来了解一种相对简单但容易出错的算法——二分查找。这里还有一些小方法让二分查找没有那么容易出错&#xff0c;开始学习吧啦啦啦&#xff01; PS&#xff1a; 文章主要参考…

设计模式学习笔记 - 设计模式与范式 - 创建型:7.原型模式:如何快速地clone一个HashMap散列表

原型模式的原理与应用 如果对象的创建成本比较大&#xff0c;而同一个类的不同对象之间差别不大&#xff08;大部分字段都相同&#xff09;&#xff0c;在这种情况下&#xff0c;我们可以利用对已有对象&#xff08;原型&#xff09;进行复制&#xff08;或者叫拷贝&#xff0…

Lunule: An Agile and Judicious Metadata Load Balancer for CephFS——论文阅读

SC 2021 Paper 分布式元数据论文阅读笔记 问题 CephFS采用动态子树分区方法&#xff0c;将分层命名空间划分并将子树分布到多个元数据服务器上。然而&#xff0c;这种方法存在严重的不平衡问题&#xff0c;由于其不准确的不平衡预测、对工作负载特性的忽视以及不必要/无效的迁…

解码新时代内存架构:探秘数据在内存中的灵动驻足

欢迎来到白刘的领域 Miracle_86.-CSDN博客 系列专栏 C语言知识 先赞后看&#xff0c;已成习惯 创作不易&#xff0c;多多支持&#xff01; 随着信息技术的飞速发展&#xff0c;我们身处一个数据爆炸的时代。数据的处理和存储方式正日益成为技术革新的重要领域。在新时代的…

【Java】高级篇2:多线程

一、相关概念 注意&#xff1a; 1、不同进程之间不共享内存 2、进程之间的数据交换和通信成本很高 线程调度&#xff1a; 单核CPU与多核CPU&#xff1a; 并行与并发&#xff1a; 二、创建和启动线程 1、概述 2、方式 2.1 方式一&#xff1a;继承Thread类 2.2 方式二&#xf…

Fantasy RPG Spell Pack 2

介绍奇幻角色扮演游戏魔法包VFX,这是为您的Unity奇幻角色扮演游戏提供的终极视觉效果解决方案!这个包包含30个独特的VFX,将为您的法术和能力带来生命,让您的玩家沉浸在魔法和奇迹的世界中。 从令人惊叹的彩虹盾和闪电到旋转门户和召唤圈,这个包有你需要的一切来创造一个真…

GETSHELL方法总结上

渗透的总步骤 1.信息收集找到弱漏洞 2.漏洞挖掘 漏洞验证 3.有一定权限 getshell 4.提权后---渗透 5.内网渗透】 前后台拿shell方法汇总 接下来我们实操一波dedecms也就是织梦cms 如果你们的靶场是空白的 可能是php版本 我们修改为5.2 可能是源码问题 我们不要急着上传…

ChatGPT论文指南|揭秘8大ChatGPT提示词研究技巧提升写作效率【建议收藏】

点击下方▼▼▼▼链接直达AIPaperPass &#xff01; AIPaperPass - AI论文写作指导平台 公众号原文▼▼▼▼&#xff1a; ChatGPT论文指南|揭秘8大ChatGPT提示词研究技巧提升写作效率【建议收藏】 目录 1.写作方法 2.方法设计 3.研究结果 4.讨论写作 5.总结结论 6.书…

MySQL--select count(*)、count(1)、count(列名) 的区别你知道吗?

MySQL select count(*)、count(1)、count(列名) 的区别&#xff1f; 这里我们先给出正确结论&#xff1a; count(*)&#xff0c;包含了所有的列&#xff0c;会计算所有的行数&#xff0c;在统计结果时候&#xff0c;不会忽略列值为空的情况。count(1)&#xff0c;忽略所有的列…

Axure RP 9 for mac中文版密钥激活版下载

Axure RP 9是一款专业的快速原型设计工具&#xff0c;它可以帮助产品设计师、交互设计师和用户体验设计师等创建高保真度、交互性强的原型&#xff0c;以便在产品开发之前进行测试和用户验证。 软件下载&#xff1a;Axure RP 9 for mac中文版密钥激活版下载 该工具具有丰富的功…

2023蓝桥杯C/C++A组省赛 B题: 有奖问答|DFS搜索 、线性dp

题目链接&#xff1a; 1.有奖问答 - 蓝桥云课 (lanqiao.cn) 说明&#xff1a; DFS做法&#xff1a; 因为是填空题&#xff0c;不用考虑超时&#xff0c;首先先考虑暴力做法DFS来做&#xff0c;根据题意&#xff0c;30道题&#xff0c;有一个答题的先后顺序&#xff0c;上一…

【算法篇】逐步理解动态规划1(斐波那契数列模型)

目录 斐波那契数列模型 1. 第N个泰波那契数 2.使用最小花费爬楼梯 3.解码方法 学过算法的应该知道&#xff0c;动态规划一直都是一个非常难的模块&#xff0c;无论是状态转移方程的定义还是dp表的填表&#xff0c;都非常难找到思路。在这个算法的支线专题中我会结合很多力…

Java学习笔记 | Java基础语法 | 03 | 流程控制语句

文章目录 0 前言1.流程控制语句1.1 流程控制语句分类1.2 顺序结构 2.判断语句2.1 if语句1. if语句格式1练习1&#xff1a;老丈人选女婿练习2&#xff1a;考试奖励 2. if语句格式2练习1&#xff1a;吃饭练习2&#xff1a;影院选座 3. if语句格式3练习1&#xff1a;考试奖励 2.2 …

Vue使用font-face自定义字体详解

目录 1 介绍2 使用2.1 语法2.2 属性说明2.3 Vue使用案例2.3.1 全局定义字体2.3.2 在页面使用 3 注意事项 1 介绍 font-face 是 CSS 中的一个规则&#xff0c;它允许你加载服务器上的字体文件&#xff08;远程或者本地&#xff09;&#xff0c;并在网页中使用这些字体。这样&am…

使用 STL 容器发生异常的常见原因分析与总结

目录 1、概述 2、使用STL列表中的元素越界 3、遍历STL列表删除元素时对迭代器自加处理有问题引发越界 4、更隐蔽的遍历STL列表删除元素时引发越界的场景 5、多线程同时操作STL列表时没有加锁导致冲突 6、对包含STL列表对象的结构体进行memset操作导致STL列表对象内存出异…

大学教材《C语言程序设计》(浙大版)课后习题解析 | 第一、二章

概述 本文主要提供《C语言程序设计》(浙大版) 第一、二章课后习题解析&#xff0c;以方便同学们完成题目后作为参考对照。后续将写出三、四章节课后习题解析&#xff0c;如想了解更多&#xff0c;请持续关注该专栏。 专栏直达链接&#xff1a;《C语言程序设计》(浙大版)_孟俊宇…