阿里二面:谈谈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;
}
ThreadLocalMap是ThreadLocal类的一个静态内部类,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对象的下标位置是通过ThreadLocal的threadLocalHashCode计算出来的。
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对象。Thread、ThreadLocal、ThreadLocalMap以及Entry数组的关系如下图:

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

当线程执行完任务并正常退出后,线程与内部ThreadLocalMap实例之间的强引用关系随之断开,这意味着线程不再持有ThreadLocalMap的引用。在这种情况下,失去强引用的ThreadLocalMap对象将符合垃圾收集器(GC)的回收条件,进而被自动回收。与此同时,鉴于ThreadLocalMap内部的键(ThreadLocal对象)是弱引用,一旦ThreadLocalMap被回收,若此时没有其他强引用指向这些ThreadLocal对象,它们也将被GC一并回收。因此,在线程结束其生命周期后,与之相关的ThreadLocalMap及其包含的ThreadLocal对象理论上都能够被正确清理,避免了内存泄漏问题。
实际应用中还需关注
ThreadLocalMap中存储的值(非键)是否为强引用类型,因为即便键(ThreadLocal对象)被回收,如果值是强引用且没有其他途径释放,仍可能导致内存泄漏。
ThreadLocal在哪些场景下会出现内存泄露?
在实际项目开发中,如果为每个任务都手动创建线程,这是一件很耗费资源的方式,并且在阿里巴巴的开发规范中也提到,不推荐使用手动创建线程,推荐使用线程池来执行相对应的任务。那么当我们使用线程池时,线程池中的线程跟ThrealLocalMap的引用关系如下:

在使用线程池处理任务时,每一个线程都会关联一个独立的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中的四种引用类型?

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

在线程池环境下使用ThreadLocal存储数据时,内存泄露的风险主要源自于线程生命周期管理及ThreadLocalMap内部结构的设计。由于线程池中的核心线程在完成任务后会复用,每个线程都会维持对各自关联的ThreadLocalMap对象的强引用,这确保了只要线程持续存在,其对应的ThreadLocalMap就无法被垃圾收集器(GC)自动回收。
进一步分析,ThreadLocalMap内部采用一个Entry数组来保存键值对,其中每个条目的Key是当前线程中对应ThreadLocal实例的弱引用,这意味着当外部不再持有该ThreadLocal实例的强引用时,Key部分能够被GC正常回收。然而,关键在于Entry的Value部分,它直接或间接地持有着强引用的对象,即使Key因为弱引用特性被回收,但Value所引用的数据却不会随之释放,除非明确移除或者整个ThreadLocalMap随着线程结束而失效。
所以,在线程池中,如果未正确清理不再使用的ThreadLocal变量,其所持有的强引用数据将在多个任务执行过程中逐渐积累并驻留在线程的ThreadLocalMap中,从而导致潜在的内存泄露风险。
ThreadLocal如何避免内存泄漏
经过上述ThreadLocal原理以及发生内存泄漏的分析,我们知道防止内存泄漏,我们一定要在完成线程内的任务后,调用ThreadLocal的remove()方法来清除当前线程中ThreadLocal所对应的值。其remove方法源码如下:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this);}}
在remove()方法中,首先根据当前线程获取ThreadLocalMap类型的对象,如果不为空,则直接调用该对象的有参remove()方法移除value的值。ThreadLocalMap的remove方法源码如下:
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()方法知道ThreadLocal中Entry下标是通过计算ThreadLocal的hashCode获得了,而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的结构图如下:

在
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的内存泄漏问题?问麻了。。。。
引言 ThreadLocal在Java多线程编程中扮演着重要的角色,它提供了一种线程局部存储机制,允许每个线程拥有独立的变量副本,从而有效地避免了线程间的数据共享冲突。ThreadLocal的主要用途在于,当需要为每个线程维护一个独立的上下文…...
IOS面试题编程机制 46-50
46. 阐述 Method Swizzle(黑魔法),什么情况下会使用?1). 在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写、和借助类别重名方法暴力抢先之外,还有更加灵活的方法 Method Swizzle。 2). Method Swizzle 指的是改变一个已存在的选择器对应的实现…...
web表单标签与练习(3.18)
一、表单域 表单域是一个包含表单元素的区域。 在HTML标签中,< form >标签用于定义表单域,以实现用户信息和传递。 < form >会把它范围内的表单元素信息提交给服务器。 表单属性 action url地址 用于指定接收并处理表单数据的服务器程序的…...
【协议-HTTP】
HTTP协议 HTTP协议(超文本传输协议HyperText Transfer Protocol),它是基于TCP协议的应用层传输协议。http协议定义web客户端如何才能够web服务器请求web页面,以及服务器如何把web页面传送给客户端。 HTTP 是一种无状态 (stateless) 协议, HTTP协议本身…...
VUE3v-text、v-html、:style的理解
在Vue 3中,v-text、v-html和:style是三个常用的指令,它们各自具有不同的功能和用途。 v-text: v-text用于操作元素中的纯文本内容。它接受一个表达式,并将该表达式的值设置为元素的文本内容。如果元素原本有文本内容,…...
Dataset之UCI_autos_cars:UCI_autos_imports-85(汽车进口数据集)的简介、安装、案例应用之详细攻略
Dataset之UCI_autos_cars:UCI_autos_imports-85(汽车进口数据集)的简介、安装、案例应用之详细攻略 目录 UCI_autos_imports-85的简介 UCI_autos_imports-85的安装 UCI_autos_imports-85的案例应用 1、训练一个简单的线性回归模型来预测汽车的价格 UCI_autos_i…...
结构体类型详细讲解(附带枚举,联合)
前言: 如果你还对结构体不是很了解,那么本篇文章将会从 为什么存在结构体,结构体的优点,结构体的定义,结构体的使用与结构体的大小依次介绍,同样会附带枚举与联合体 目录 为什么存在结构体: 结构…...
编程生活day1--个位数统计、考试座位号、A-B、计算阶乘和
个位数统计 题目描述: 定一个 k 位整数 Ndk−110k−1⋯d1101d0 (0≤di≤9, i0,⋯,k−1, dk−1>0),请编写程序统计每种不同的个位数字出现的次数。例如:给定 N100311,则有 2 个 0,3 个 1,和 …...
mysql体系结构及主要文件
目录 1.mysql体系结构 2.数据库与数据库实例 3.物理存储结构编辑 4.mysql主要文件 4.1数据库配置文件 4.2错误日志 4.3表结构定义文件 4.4慢查询日志 4.4.1慢查询相关参数 4.4.2慢查询参数默认值 4.4.3my.cnf中设置慢查询参数 4.4.4slow_query_log参数 4.4.…...
PwnLab靶场PHP伪协议OSCP推荐代码审计命令劫持命令注入
下载链接:PwnLab: init ~ VulnHub 安装: 打开vxbox直接选择导入虚拟电脑即可 正文: 先用nmap扫描靶机ip nmap -sn 192.168.1.1/24 获取到靶机ip后,对靶机的端口进行扫描,并把结果输出到PwnLab文件夹下,命名…...
涉密信息系统集成资质八大类别办理条件是什么?
涉密资质分为八个不同类别,那每个类别的申报条件有哪些?让我们一起来看看吧: 涉密资质申报条件 依据《涉密信息系统集成资质管理办法》的有关规定,申请涉密信息系统集成资质的企事业单位,除符合《涉密信息系统集成资…...
Shell脚本总结-反引号-${}-$()
反引号 反引号的作用就是将输出结果显示出来。 [rootldpbzhaonan bash]$ echo $a ldpbzhaonan${} ${}引用变量,包含自定义的和环境变量。 [rootldpbzhaonan bash]$ a1 [rootldpbzhaonan bash]$ echo ${a} 1$() $()和反引号,返回的是一个指令或者程序…...
Spring MVC入门(4)
请求 获取Cookie/Session 获取Cookie 传统方式: RequestMapping("/m11")public String method11(HttpServletRequest request, HttpServletResponse response) {//获取所有Cookie信息Cookie[] cookies request.getCookies();//打印Cookie信息StringBuilder build…...
RuoYi-Vue-Plus(基础知识点jackson、mybatisplus、redis)
一、JacksonConfig 全局序列化反序列化配置 1.1yml中配置 #时区 spring.jackson.time-zoneGMT8 #日期格式 spring.jackson.date-formatyyyy-MM-dd HH:mm:ss #默认转json的属性,这里设置为非空才转json spring.jackson.default-property-inclusionnon_null #设置属性…...
使用verillog编写KMP字符串匹配算法
设计思路如下: 定义模块的输入输出信号:包括时钟信号clk、复位信号rst、模式串pattern、文本串text以及输出信号match。定义所需寄存器和变量:使用寄存器来存储状态机的状态以及其他控制变量,如模式串数组P、失配函数数组F、模式串位置p_index、文本串位置t_index等。在时钟…...
《每天十分钟》-红宝书第4版-对象、类与面向对象编程(五)
对象迭代 在 JavaScript 有史以来的大部分时间内,迭代对象属性都是一个难题。ECMAScript 2017 新增了两个静态方法,用于将对象内容转换为序列化的——更重要的是可迭代的——格式。这两个静态方法Object.values()和 Object.entries()接收一个对象&#…...
华为ensp中rip动态路由协议原理及配置命令(详解)
CSDN 成就一亿技术人! 作者主页:点击! ENSP专栏:点击! CSDN 成就一亿技术人! ————前言————— RIP(Routing Information Protocol,路由信息协议)是一种距离矢…...
学习要不畏难
我突然发现,畏难心是阻碍我成长的最大敌人。事未难,心先难,心比事都难,是我最大的毛病。然而一念由心生,心不难时,则真难事也不再难。很多那些自认为很难的事,硬着头皮做下来的时候,…...
mysql迁移达梦数据库 Java踩坑合集
达梦数据库踩坑合集 文章目录 安装达梦设置大小写不敏感Spring boot引入达梦驱动(两种方式)将jar包打入本地maven仓库使用国内maven仓库(阿里云镜像) 达梦驱动yml配置springboot mybatis-plus整合达梦,如何避免指定数据库名&…...
arm 解决Rk1126 画框颜色变色问题(RGB转NV12)
在Rv1126上直接对Nv12图像进行绘制时,颜色是灰色。故将Nv12转BGR后绘制图像,绘制完成后转成Nv12,BGR的图像颜色是正常的,但是NV12的图像颜色未画全,如图: 1.排查发现是RGB转NV12的函数出现问题,…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...
【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...
html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...
