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

2024-03-25 03:20

本文主要是介绍阿里二面:谈谈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);elsecreateMap(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;elsereturn 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 staleSlottab[staleSlot].value = null;tab[staleSlot] = null;size--;// Rehash until we encounter nullEntry 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、中间件、架构设计、面试题、程序员攻略等。

这篇关于阿里二面:谈谈ThreadLocal的内存泄漏问题?问麻了。。。。的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/843706

相关文章

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

阿里开源语音识别SenseVoiceWindows环境部署

SenseVoice介绍 SenseVoice 专注于高精度多语言语音识别、情感辨识和音频事件检测多语言识别: 采用超过 40 万小时数据训练,支持超过 50 种语言,识别效果上优于 Whisper 模型。富文本识别:具备优秀的情感识别,能够在测试数据上达到和超过目前最佳情感识别模型的效果。支持声音事件检测能力,支持音乐、掌声、笑声、哭声、咳嗽、喷嚏等多种常见人机交互事件进行检测。高效推

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo

题目1254:N皇后问题

题目1254:N皇后问题 时间限制:1 秒 内存限制:128 兆 特殊判题:否 题目描述: N皇后问题,即在N*N的方格棋盘内放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在同一斜线上。因为皇后可以直走,横走和斜走如下图)。 你的任务是,对于给定的N,求出有多少种合法的放置方法。输出N皇后问题所有不同的摆放情况个数。 输入

vscode中文乱码问题,注释,终端,调试乱码一劳永逸版

忘记咋回事突然出现了乱码问题,很多方法都试了,注释乱码解决了,终端又乱码,调试窗口也乱码,最后经过本人不懈努力,终于全部解决了,现在分享给大家我的方法。 乱码的原因是各个地方用的编码格式不统一,所以把他们设成统一的utf8. 1.电脑的编码格式 开始-设置-时间和语言-语言和区域 管理语言设置-更改系统区域设置-勾选Bata版:使用utf8-确定-然后按指示重启 2.vscode