Java并发编程笔记之ThreadLocal内存泄漏探究

使用 ThreadLocal 不当可能会导致内存泄露,是什么原因导致的内存泄漏呢?

我们首先看一个例子,代码如下:

/*** Created by cong on 2018/7/14.*/
public class ThreadLocalOutOfMemoryTest {static class LocalVariable {private Long[] a = new Long[1024*1024];}// (1)final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES,new LinkedBlockingQueue<>());// (2)final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();public static void main(String[] args) throws InterruptedException {// (3)for (int i = 0; i < 50; ++i) {poolExecutor.execute(new Runnable() {public void run() {// (4)localVariable.set(new LocalVariable());// (5)System.out.println("use local varaible");
//                    localVariable.remove();
}});Thread.sleep(1000);}// (6)System.out.println("pool execute over");}
}

代码(1)创建了一个核心线程数和最大线程数为 6 的线程池,这个保证了线程池里面随时都有 6 个线程在运行。

代码(2)创建了一个 ThreadLocal 的变量,泛型参数为 LocalVariable,LocalVariable 内部是一个 Long 数组。

代码(3)向线程池里面放入 50 个任务。

代码(4)设置当前线程的 localVariable 变量,也就是把 new 的 LocalVariable 变量放入当前线程的 threadLocals 变量。

由于没有调用线程池的 shutdown 或者 shutdownNow 方法所以线程池里面的用户线程不会退出,进而 JVM 进程也不会退出。

 

运行后,我们立即打开jconsole 监控堆内存变化,如下图:

接着,让我们打开 localVariable.remove() 注释,然后在运行,观察堆内存变化如下:

 

 从第一次运行结果可知,当主线程处于休眠时候进程占用了大概 75M 内存,打开 localVariable.remove() 注释后第二次运行则占用了大概 25M 内存,可知 没有写 localVariable.remove() 时候内存发生了泄露,下面分析下泄露的原因,如下:

第一次运行的代码,在设置线程的 localVariable 变量后没有调用localVariable.remove() 方法,导致线程池里面的 5 个线程的 threadLocals 变量里面的new LocalVariable()实例没有被释放,虽然线程池里面的任务执行完毕了,但是线程池里面的 5 个线程会一直存在直到 JVM 退出。这里需要注意的是由于 localVariable 被声明了 static,虽然线程的 ThreadLocalMap 里面是对 localVariable 的弱引用,localVariable 也不会被回收。运行结果二的代码由于线程在设置 localVariable 变量后即使调用了localVariable.remove()方法进行了清理,所以不会存在内存泄露。

 

接下来我们要想清楚的知道内存泄漏的根本原因,那么我们就要进入源码去看了。

我们知道ThreadLocal 只是一个工具类,具体存放变量的是在线程的 threadLocals 变量里面,threadLocals 是一个 ThreadLocalMap 类型的,我们首先一览ThreadLocalMap的类图结构,类图结构如下图:

 如上图 ThreadLocalMap 内部是一个 Entry 数组, Entry 继承自 WeakReference,Entry 内部的 value 用来存放通过 ThreadLocal 的 set 方法传递的值,那么 ThreadLocal 对象本身存放到哪里了吗?

下面看看 Entry 的构造函数,如下所示:

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

接着我们再接着看Entry的父类WeakReference的构造函数super(k),如下所示:

public WeakReference(T referent) {super(referent);
}

接着我们再看WeakReference的父类Reference的构造函数super(referent),如下所示:

Reference(T referent) {this(referent, null);
}

接着我们再看WeakReference的父类Reference的另外一个构造函数this(referent , null),如下所示:

Reference(T referent, ReferenceQueue<? super T> queue) {this.referent = referent;this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

可知 k 被传递到了 WeakReference 的构造函数里面,也就是说 ThreadLocalMap 里面的 key 为 ThreadLocal 对象的弱引用,具体是 referent 变量引用了 ThreadLocal 对象,value 为具体调用 ThreadLocal 的 set 方法传递的值。

当一个线程调用 ThreadLocal 的 set 方法设置变量时候,当前线程的 ThreadLocalMap 里面就会存放一个记录,这个记录的 key 为 ThreadLocal 的引用,value 则为设置的值。

但是考虑如果这个 ThreadLocal 变量没有了其他强依赖,而当前线程还存在的情况下,由于线程的 ThreadLocalMap 里面的 key 是弱依赖,则当前线程的 ThreadLocalMap 里面的 ThreadLocal 变量的弱引用会被在 gc 的时候回收,但是对应 value 还是会造成内存泄露,这时候 ThreadLocalMap 里面就会存在 key 为 null 但是 value 不为 null 的 entry 项。

其实在 ThreadLocal 的 set 和 get 和 remove 方法里面有一些时机是会对这些 key 为 null 的 entry 进行清理的,但是这些清理不是必须发生的,下面简单讲解ThreadLocalMap 的 remove 方法的清理过程,remove 的源码,如下所示:

private void remove(ThreadLocal<?> key) {//(1)计算当前ThreadLocal变量所在table数组位置,尝试使用快速定位方法Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);//(2)这里使用循环是防止快速定位失效后,变量table数组for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//(3)找到if (e.get() == key) {//(4)找到则调用WeakReference的clear方法清除对ThreadLocal的弱引用
          e.clear();//(5)清理key为null的元素
          expungeStaleEntry(i);return;}}
}private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;//(6)去掉去value的引用tab[staleSlot].value = null;tab[staleSlot] = null;size--;
Entry e;
int i;for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//(7)如果key为null,则去掉对value的引用。if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}

代码(4)调用了 Entry 的 clear 方法,实际调用的是父类 WeakReference 的 clear 方法,作用是去掉对 ThreadLocal 的弱引用。

代码(6)是去掉对 value 的引用,到这里当前线程里面的当前 ThreadLocal 对象的信息被清理完毕了。

代码(7)从当前元素的下标开始看 table 数组里面的其他元素是否有 key 为 null 的,有则清理。循环退出的条件是遇到 table 里面有 null 的元素。所以这里知道 null 元素后面的 Entry 里面 key 为 null 的元素不会被清理。

总结:

  1.ThreadLocalMap 内部 Entry 中 key 使用的是对 ThreadLocal 对象的弱引用,这为避免内存泄露是一个进步,因为如果是强引用,那么即使其他地方没有对 ThreadLocal 对象的引用,ThreadLocalMap 中的 ThreadLocal 对象还是不会被回收,而如果是弱引用则这时候 ThreadLocal 引用是会被回收掉的。

  2.但是对于的 value 还是不能被回收,这时候 ThreadLocalMap 里面就会存在 key 为 null 但是 value 不为 null 的 entry 项,虽然 ThreadLocalMap 提供了 set,get,remove 方法在一些时机下会对这些 Entry 项进行清理,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露,所以在使用完毕后即使调用 remove 方法才是解决内存泄露的最好办法。

  3.线程池里面设置了 ThreadLocal 变量一定要记得及时清理,因为线程池里面的核心线程是一直存在的,如果不清理,那么线程池的核心线程的 threadLocals 变量一直会持有 ThreadLocal 变量。

转载于:https://www.cnblogs.com/huangjuncong/p/9311308.html

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

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

相关文章

Res协议,专题

打造完美的&#xff29;&#xff25;网页木马 发表&#xff1a;2004-5-20 19:41:00 出处&#xff1a;你的博客网(yourblog.org) 打造完美的&#xff29;&#xff25;网页木马 icyfox &#xff08;测试页面:http://www.godog.y365.com/runexe/icyfox.htm&#xff0c;此处所…

springboot中获取bean_最新Spring Boot干货总结(超详细,建议收藏)

前言&#xff1a;本文非常长&#xff0c;建议先mark后看&#xff0c;也许是最后一次写这么长的文章说明&#xff1a;前面有4个小节关于Spring的基础知识分别是&#xff1a;IOC容器、JavaConfig、事件监听、SpringFactoriesLoader详解它们占据了本文的大部分内容&#xff1a;虽然…

python全栈学习总结三:函数学习

一函数基本概念 1 函数定义 def 函数名&#xff08;参数1&#xff0c;参数2&#xff0c;参数3&#xff0c;......&#xff09;: """ 功能&#xff1a; 参数1&#xff1a; 参数2&#xff1a; 参数3&#xff1a; ...... 返回值&#xff1a; """ 函…

街机模拟器联网简谈

Kawaks联网教程现在大多数模拟器的联网功能主要是靠"kaillera"来实现的,要想实现联网玩游戏,你必须有一个kaillera的客户端和一个服务器端的程序,客户端现在大多在模拟器中自带,而服务器端则是一个单独的程序.kaillera有多种操作系统,而大多是在windows下,好了废话不…

freeRTOS V10.0.1移植到STM32F407标准库 - 环境Keil5

最近因为工作需要用到FreeRTOS&#xff0c;其实开始本人内心是拒绝的因为自己只学习过UCOSIII还没实际上过什么大又复杂的工程&#xff0c;但是谁让FreeRTOS他是Free的呢公司成本考虑肯定是不会选择USOS的&#xff0c;这个道理就像公司内心深处不想给你涨工资一样。好了跑偏了言…

破解静态WEP KEY全过程

破解静态WEP KEY全过程广州卓讯盟科技有限公司 黄超毅Email:huangchaoyigzpia.com/demonalexdark2s.org发现首先通过NetStumbler确认客户端已在某AP的覆盖区内&#xff0c;并通过AP信号的参数进行‘踩点’&#xff08;数据搜集&#xff09;。NetStumbler 下载地址 http://www.n…

apicloud项目怎么运行_Spring Boot教程(3) – 运行第一个项目

把项目运行起来是极具成就感的事情&#xff0c;不管是在学习新语言还是新框架的过程中。上一篇文章说明了如何使用Spring Initializr和IDE来创建项目&#xff0c;下面就看看项目能不能跑起来&#xff0c;我们通过IDE和命令行&#xff0c;直观地理解框架的作用。先来一张Intelli…

Matlab从入门到精通 Chapter5 数据可视化

5-1 图形绘制示例 >> x2-17:0.02:3;>> y21./((x23).^21)1./((x29).^24)5; >> subplot(2,2,2);>> plot(x2,y2,rp);>> axis([-17 3 5 6.5]);>> title(figure2);>> grid on subplot 子图表绘制函数 默认情况下&#xff0c;plot函数将绘…

破解WEP密钥过程全解 (下)

三、实战破解过程1、用Kismet进行网络探测Kismet是一个基于Linux的无线网络扫描程序&#xff0c;这是一个相当方便的工具&#xff0c;通过测量周围的无线信号来找到目标WLAN。虽说Kismet也可以捕获网络上的数据通信&#xff0c;但在还有其他更好的工具使用(如Airodump)&#xf…

mpAndroidchart 坐标和图表距离_【玩转图表系列】六步,美化你的图表,让老板刮目相看!...

近期隆重推出图表分析系列&#xff0c;包括销售分析、盈亏分析、费用分析、趋势分析、进度分析等&#xff0c;通过双坐标图、甘特图、瀑布图、双层饼图等系列图表精美展现&#xff0c;专业高效&#xff0c;让你从初级学到高级、从小白跨越专业。今天开启我们邦邦财玩转图表第一…

SpringBoot+FreeMarker开发word文档下载,预览

背景&#xff1a; 开发一个根据模版&#xff0c;自动填充用户数据并下载word文档的功能 使用freemarker进行定义模版&#xff0c;然后把数据进行填充。 maven依赖&#xff1a; <parent> <groupId>org.springframework.boot</groupId> <artifactId>…

使用 IPsec 与组策略隔离服务器和域-第 7 章 IPsec 疑难解答

本章提供有关如何对 Internet 协议安全性 (IPsec) 问题&#xff08;如服务器和域隔离方案中的安全性问题&#xff09;进行疑难解答的信息&#xff0c;这些信息依赖于 Microsoft 信息技术 (IT) 小组的经验和方法。 在有可能的时候&#xff0c;本章将引用现有的 Microsoft 疑难解…

单片机的单个IO口可以发送数据吗_关于51单片机各个引脚它的功能你了解多少?...

对于40引脚双列直插51单片机各个引脚功能情况分析。P0端口一默认是开漏准双向IO口&#xff0c;没有输出没有驱动能力&#xff0c;要做逻辑输出要在外部接上拉电阻。P0端口除了作为普通的输入输出功能&#xff0c;还可以用来当做数据总线接口或者地址总线&#xff0c;当外面扩展…

git merge用法_常用命令之git操作(进阶篇)

几乎每一种版本控制系统都以某种形式支持分支。使用分支意味着你可以从开发主线上分离开来&#xff0c;然后在不影响主线的同时继续工作。有人把 Git 的分支模型称为必杀技特性&#xff0c;而正是因为它&#xff0c;将 Git 从版本控制系统家族里区分出来。不同的分支可以理解为…

2G内存刚刚好 浅谈4G内存对笔记本的影响

随着Intel P3x系列芯片组的发布&#xff0c;DDR3内存的普及日益临近&#xff0c;AMD K10系列处理器AM2规格也正式支持DDR3内存&#xff0c;明年即将面临淘汰的DDRII内存是越来越便宜了。很多笔记本用户在购买笔记本的时候都把是否支持4G内存作为一个总要的衡量标准。官方资料里…

由object元素引出的事件注册问题和层级显示问题

项目有一个双击监控视频全屏的需求&#xff0c;视频播放使用的是IE下的ActiveX控件&#xff0c;web页面中使用HTML嵌入对象元素object。预期方案如下&#xff1a; 1.在开发ActiveX控件时加入双击事件。 2.通过div包裹object&#xff0c;控制div的大小从而间接实现全屏效果。 在…

cad钣金展开插件_钣金高级工考试大小头手工展开图步骤教程

提醒&#xff1a;点上方↑↑↑“钣金学习网”免费关注简单方法一: 等分弧画法简单方法二: 角度计算画法1、先画一条中心直线&#xff0c;在线上取一点A&#xff0c;过&#xff21;点画一条与中心线垂直相交线。量取大头半径以&#xff21;点为起点在相交线上标注一点&#…

介绍一个新鲜玩意 开源的杀毒软件

有“免费的午餐”我们为什么不吃呢&#xff1f;杀毒软件一定要购买或用D版吗&#xff1f;先别忙着下结论&#xff0c;请耐心看完本文&#xff0c;然后再告诉我你是怎么想的。&#xff08;一&#xff09;、ClamWin Free Antivirus 开源反病毒软件 GPL协议SourceForge页面&#x…

没有可用软件包 jenkins。_Jenkins分布式构建与并行构建

Jenkins分布式构建与并行构建 jenkins的架构 Jenkins采用的是“masteragent&#xff08;slave&#xff09;”架构。Jenkins master负责提供界面、处理HTTP请求及管理构建环境&#xff1b;构建的执行则由Jenkins agent负责 Jenkins agent的横向扩容&#xff1a;只需要增加agent就…

qstring去掉特定字符_如何花式、批量且操作简单地处理字符?

文本字符的规范化&#xff0c;是批量处理的前提。多来源数据或是数据收集本身就必然存在不规范情况&#xff0c;于是批量地完成它就成了常见的需求。对于花式批量处理&#xff0c;自然要说到正则表达式。“正则表达式是对字符串操作的一种逻辑公式&#xff0c;就是用事先定义好…