阿里二面:谈谈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);
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
对象的下标位置是通过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;
else
return 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 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
的结构图如下:
在
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以及内存泄漏
ThreadLocal是什么 ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度.但是如果滥用Thr ...
- 强软弱引用,ThreadLocal和内存泄漏
强引用 写法:Object obj=new Object() 引用强度:最强 只要被引用着,就不会被gc(垃圾回收)回收掉. 软引用 写法:SoftReference<String> sr ...
- 18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题
1. 造成内存泄漏的原因? threadLocal是为了解决对象不能被多线程共享访问的问题,通过threadLocal.set方法将对象实例保存在每个线程自己所拥有的threadLocalMap中,这 ...
- ThreadLocal 内存泄漏问题深入分析
写在前面 ThreadLocal 基本用法本文就不介绍了,如果有不知道的小伙伴可以先了解一下,本文只研究 ThreadLocal 内存泄漏这一问题. ThreadLocal 会发生内存泄漏吗? 先给出 ...
- Android防止内存泄漏以及MAT的使用
Android发生内存泄漏最普遍的一种情况就是长期保持对Context,特别是Activity的引用,使得Activity无法被销毁.这也就意味着Activity中所有的成员变量也没办法销毁.本文仅介 ...
- VC使用CRT调试功能来检测内存泄漏
信息来源:csdn C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话.在 C/C+ ...
- android 常见内存泄漏原因及解决办法
android常见内存泄漏主要有以下几类: 一.Handler 引起的内存泄漏. 在Android开发中,我们经常会使用Handler来控制主线程UI程序的界面变化,使用非常简单方便,但是稍不注意,很 ...
- Windows内存性能分析(一)内存泄漏
判断内存性能表现主要是为了解决如下两个问题: 1. 当前web应用是否存在内存泄漏,如果有,问题的程度有多大? 2. 如果web应用的代码无法进一步改进,当前web应用所在的服务器是否存在内存上的瓶颈 ...
- 面试官:小伙子,你给我说一下Java中什么情况会导致内存泄漏呢?
概念 内存泄露:指程序中动态分配内存给一些临时对象,但对象不会被GC回收,它始终占用内存,被分配的对象可达但已无用.即无用对象持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间浪费. 可达 ...
- 并发编程(四):ThreadLocal从源码分析总结到内存泄漏
一.目录 1.ThreadLocal是什么?有什么用? 2.ThreadLocal源码简要总结? 3.ThreadLocal为什么会导致内存泄漏? 二.ThreadLoc ...
随机推荐
- 欧拉公式 Euler's Formula
欧拉公式是数学中最重要的公式之一, 它涉及到了复数, 无理数, 三角函数, 简单优美 \(e^{i\theta} = cos(\theta) + isin(\theta)\) 欧拉公式代表的含义并不是 ...
- 【Unity3D】卷轴特效
1 原理 当一个圆在地面上沿直线匀速滚动时,圆上固定点的运动轨迹称为旋轮线(或摆线.圆滚线).本文实现的卷轴特效使用了旋轮线相关理论. 以下是卷轴特效原理及公式推导,将屏幕坐标 (x) 映射到 ...
- SpringBoot+Shiro+LayUI权限管理系统项目-4.实现部门管理
1.说明 只讲解关键部分,详细看源码,文章下方捐赠或QQ联系捐赠获取. 2.功能展示 3.业务模型 @Data @EqualsAndHashCode(callSuper = false) @Acces ...
- 敏感信息泄露之如何隐藏IIS服务器名称和版本号
1.问题说明 请求IIS部署的网站可以发现响应头中暴露了IIS服务器名称/版本号. 漏洞等级:中 2.解决方案 想办法隐藏掉这部分信息. 2.1 下载并安装微软官方IIS扩展插件 URL Rewrit ...
- Java中交换2个变量的三种方式
这一题是我之前找Java工作时的笔试题,比较有代表性,拿出来和大家分享. package com.dylan.practice.interview; /** * 交换2个整形变量的几种方式 * * @ ...
- ERROR 1820 (HY000): You must reset your password using ALTER USER statement
新安装好的mysql5.7数据库,用root登录以后执行操作报这个错. 解决方法: mysql> alter user 'root'@'localhost' identified by 'roo ...
- Spring源码之spring事务
目录 Spring事务 事务自定义标签 自定义标签 解析标签 bean 的初始化 InfrastructureAdvisorAutoProxyCreator 获取增强方法 获取所有增强中内适用于当前方 ...
- win32- GetWindowText
从编辑框中获取控件文本 一般常用的方法是, wchar_t buffer[100]; GetWindowText(hWnd, buffer, sizeof(buffer) / sizeof(buffe ...
- 【Android 逆向】【ARM汇编】 函数的栈帧
1. 函数的调用约定 ARM32 参数1-4 放入r0-r3 剩下的入栈,函数返回值放入r0 ARM64 参数1-8 放入X0-X7 剩下的入栈,函数返回值放入X0 (浮点数是放入 Dn 或 Sn) ...
- RK3568开发笔记(二):入手RK3568开发板的套件介绍、底板介绍和外设测试
前言 本篇主要介绍RK3568芯片和入手开发板的底板介绍以及开发板的外设. 开发板 笔者的开发板是全套+10.1寸屏. 开发板实物 开发板资源 开发版本提供资料 开发 ...