ThreadLocal内存溢出代码演示和原因分析!

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

前言

ThreadLocal 翻译成中文是线程本地变量的意思,也就是说它是线程中的私有变量,每个线程只能操作自己的私有变量,所以不会造成线程不安全的问题。

所谓的线程不安全是指,多个线程在同一时刻对同一个全局变量做写操作时(读操作不会涉及线程不安全问题),如果执行的结果和我们预期的结果不一致就称之为线程不安全,反之,则称为线程安全。

在 Java 语言中解决线程不安全的问题通常有两种手段

  1. 使用锁(使用 synchronized 或 Lock);

  2. 使用 ThreadLocal。

锁的实现方案是在多线程写入全局变量时,通过排队一个一个来写入全局变量,从而就可以避免线程不安全的问题了。比如当我们使用线程不安全的 SimpleDateFormat 对时间进行格式化时,如果使用锁来解决线程不安全的问题,实现的流程就是这样的:

从上述图片可以看出,通过加锁的方式虽然可以解决线程不安全的问题,但同时带来了新的问题,使用锁时线程需要排队执行,因此会带来一定的性能开销。然而,如果使用的是 ThreadLocal 的方式,则是给每个线程创建一个 SimpleDateFormat 对象,这样就可以避免排队执行的问题了,它的实现流程如下图所示:

PS:创建 SimpleDateFormat 也会消耗一定的时间和空间,如果线程复用 SimpleDateFormat 的频率比较高的情况下,使用 ThreadLocal 的优势比较大,反之则可以考虑使用锁。

然而,在我们使用 ThreadLocal 的过程中,很容易就会出现内存溢出的问题,如下面的这个事例。

什么是内存溢出?

内存溢出(Out Of Memory,简称 OOM)是指无用对象(不再使用的对象)持续占有内存,或无用对象的内存得不到及时释放,从而造成的内存空间浪费的行为就称之为内存泄露。

内存溢出代码演示

在开始演示 ThreadLocal 内存溢出的问题之前,我们先使用“-Xmx50m”的参数来设置一下 Idea,它表示将程序运行的最大内存设置为 50m,如果程序的运行超过这个值就会出现内存溢出的问题,设置方法如下:

设置后的最终效果这样的:

PS:因为我使用的 Idea 是社区版,所以可能和你的界面不一样,你只需要点击“Edit Configurations...”找到“VM options”选项,设置上“-Xmx50m”参数就可以了。

配置完 Idea 之后,接下来我们来实现一下业务代码。在代码中我们会创建一个大对象,这个对象中会有一个 10m 大的数组,然后我们将这个大对象存储在 ThreadLocal 中,再使用线程池执行大于 5 次添加任务,因为设置了最大运行内存是 50m,所以理想的情况是执行 5 次添加操作之后,就会出现内存溢出的问题,实现代码如下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class ThreadLocalOOMExample {/*** 定义一个 10m 大的类*/static class MyTask {// 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)private byte[] bytes = new byte[10 * 1024 * 1024];}// 定义 ThreadLocalprivate static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();// 主测试代码public static void main(String[] args) throws InterruptedException {// 创建线程池ThreadPoolExecutor threadPoolExecutor =new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));// 执行 10 次调用for (int i = 0; i < 10; i++) {// 执行任务executeTask(threadPoolExecutor);Thread.sleep(1000);}}/*** 线程池执行任务* @param threadPoolExecutor 线程池*/private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {// 执行任务threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {System.out.println("创建对象");// 创建对象(10M)MyTask myTask = new MyTask();// 存储 ThreadLocaltaskThreadLocal.set(myTask);// 将对象设置为 null,表示此对象不在使用了myTask = null;}});}
}

以上程序的执行结果如下:

从上述图片可看出,当程序执行到第 5 次添加对象时就出现内存溢出的问题了,这是因为设置了最大的运行内存是 50m,每次循环会占用 10m 的内存,加上程序启动会占用一定的内存,因此在执行到第 5 次添加任务时,就会出现内存溢出的问题。

原因分析

内存溢出的问题和解决方案比较简单,重点在于“原因分析”,我们要通过内存溢出的问题搞清楚,为什么 ThreadLocal 会这样?是什么原因导致了内存溢出?

要搞清楚这个问题(内存溢出的问题),我们需要从 ThreadLocal 源码入手,所以我们首先打开 set 方法的源码(在示例中使用到了 set 方法),如下所示:

public void set(T value) {// 得到当前线程Thread t = Thread.currentThread();// 根据线程获取到 ThreadMap 变量ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value); // 将内容存储到 map 中elsecreateMap(t, value); // 创建 map 并将值存储到 map 中
}

从上述代码我们可以看出 Thread、ThreadLocalMap 和 set 方法之间的关系:每个线程 Thread 都拥有一个数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set  方法执行时,会将要存储的值放到 ThreadLocalMap 容器中,所以接下来我们再看一下 ThreadLocalMap 的源码:

static class ThreadLocalMap {// 实际存储数据的数组private Entry[] table;// 存数据的方法private void set(ThreadLocal<?> key, Object value) {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)]) {ThreadLocal<?> k = e.get();// 如果有对应的 key 直接更新 value 值if (k == key) {e.value = value;return;}// 发现空位插入 valueif (k == null) {replaceStaleEntry(key, value, i);return;}}// 新建一个 Entry 插入数组中tab[i] = new Entry(key, value);int sz = ++size;// 判断是否需要进行扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}// ... 忽略其他源码
}

从上述源码我们可以看出:ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值

根据上面的内容,我们可以得出 ThreadLocal 相关对象的关系图,如下所示:

也就是说它们之间的引用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有 value 值,那么垃圾回收器就无法回收 value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生

解决方案

ThreadLocal 内存溢出的解决方案很简单,我们只需要在使用完 ThreadLocal 之后,执行 remove 方法就可以避免内存溢出问题的发生了,比如以下代码:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class App {/*** 定义一个 10m 大的类*/static class MyTask {// 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)private byte[] bytes = new byte[10 * 1024 * 1024];}// 定义 ThreadLocalprivate static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();// 测试代码public static void main(String[] args) throws InterruptedException {// 创建线程池ThreadPoolExecutor threadPoolExecutor =new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));// 执行 n 次调用for (int i = 0; i < 10; i++) {// 执行任务executeTask(threadPoolExecutor);Thread.sleep(1000);}}/*** 线程池执行任务* @param threadPoolExecutor 线程池*/private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {// 执行任务threadPoolExecutor.execute(new Runnable() {@Overridepublic void run() {System.out.println("创建对象");try {// 创建对象(10M)MyTask myTask = new MyTask();// 存储 ThreadLocaltaskThreadLocal.set(myTask);// 其他业务代码...} finally {// 释放内存taskThreadLocal.remove();}}});}
}

以上程序的执行结果如下:

从上述结果可以看出我们只需要在 finally 中执行 ThreadLocal 的 remove 方法之后就不会在出现内存溢出的问题了。

remove的秘密

那 remove 方法为什么会有这么大的魔力呢?我们打开 remove 的源码看一下:

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

从上述源码中我们可以看出,当调用了 remove 方法之后,会直接将 Thread 中的 ThreadLocalMap 对象移除掉,这样 Thread 就不再持有 ThreadLocalMap 对象了,所以即使 Thread 一直存活,也不会造成因为(ThreadLocalMap)内存占用而导致的内存溢出问题了。

总结

本文我们使用代码的方式演示了 ThreadLocal 内存溢出的问题,严格来讲内存溢出并不是 ThreadLocal 的问题,而是因为没有正确使用 ThreadLocal 所带来的问题。想要避免 ThreadLocal 内存溢出的问题,只需要在使用完 ThreadLocal 后调用 remove 方法即可。不过通过 ThreadLocal 内存溢出的问题,让我们搞清楚了 ThreadLocal 的具体实现,方便我们日后更好的使用 ThreadLocal,以及更好的应对面试。


往期推荐

ThreadLocal不好用?那是你没用对!


SimpleDateFormat线程不安全的5种解决方案!


Semaphore自白:限流器用我就对了!


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

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

相关文章

C,C++宏中#与##的讲解

文中__FILE__与示例1可以参见《使用ANSI C and Microsoft C中常用的预定义宏》宏中的#的功能是将其后面的宏参数进行字符串化操作&#xff08;Stringizing operator&#xff09;&#xff0c;简单说就是在它引用的宏变量的左右各加上一个双引号。 如定义好#define STRING(x) #x之…

int?id与id??1 的意思

http://blog.csdn.net/jingmeifeng/article/details/24710143 int? id 表示id是可以为null的整型 跟Nullable<int> id 是一样的 id ?? 1等于 idnull?1:id;

彻夜怒肝!Spring Boot+Sentinel+Nacos高并发已撸完,快要裂开了!

很多人说程序员是最容易实现财富自由的职业&#xff0c;也确实&#xff0c;比如字节 28 岁的程序员郭宇不正是从普通开发一步步做起的吗&#xff1f;回归行业现状&#xff0c;当开发能力可以满足公司业务需求时&#xff0c;拿到超预期的 Offer 并不算难。最近我也一直在思考这个…

java中get接口示例_Java LocalDateTime类| 带示例的get()方法

java中get接口示例LocalDateTime类的get()方法 (LocalDateTime Class get() method) get() method is available in java.time package. get()方法在java.time包中可用。 get() method is used to get the value for the given field from this date-time object. get()方法用于…

湖南多校对抗5.24

据说A,B,C题都比较水这里就不放代码了 D:Facility Locations 然而D题是一个脑经急转弯的题&#xff1a;有m行&#xff0c;n列&#xff0c;每个位置有可能为0&#xff0c;也可能不为0&#xff0c;问最多选K行是不是可以使得每一列都至少有一个0&#xff0c;其中代价c有个约束条件…

PPT演讲计时器

下载 GitHub 源码地址 如果访问不到的话&#xff0c;可以从百度盘下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1bK4sug-eK85fmPgi9DzhcA 提取码&#xff1a;0vp3 文件&#xff1a;VB.Equal.Timer-VB计时器软件-绿色无残留 写在前面 转眼也工作了两年了&…

2万字!66道并发面试题及答案

我花了点时间整理了一些多线程&#xff0c;并发相关的面试题&#xff0c;虽然不是很多&#xff0c;但是偶尔看看还是很有用的哦&#xff01;话不多说&#xff0c;直接开整&#xff01;01 什么是线程&#xff1f;线程是操作系统能够进⾏运算调度的最⼩单位&#xff0c;它被包含在…

stl向量_如何在C ++ STL中将数组元素复制到向量?

stl向量Given an array and we have to copy its elements to a vector in C STL. 给定一个数组&#xff0c;我们必须将其元素复制到C STL中的向量。 将数组元素复制到向量 (Copying array elements to a vector) In C STL, we can copy array elements to a vector by using…

YoloV8的目标检测推理

YoloV8的目标检测推理 原始的YoloV8封装的层次太高&#xff0c;想要为我们所用可能需要阅读很多API&#xff0c;下面给出比较简单的使用方式 导入所需的库 os&#xff1a;用于操作文件系统。cv2 (OpenCV)&#xff1a;用于图像处理。numpy&#xff1a;提供数学运算&#xff0…

【翻译】从Store生成Checkbox Group

原文&#xff1a;Ext JS: Generating a Checkbox Group from a StoreExt JS的checkbox group可以用来将复选框组合成一个单一的逻辑字段。由于复选框时不时需要动态的从Store中生成&#xff0c;因而&#xff0c;如果将store绑定到扩展类&#xff0c;就最好不过了。以下是第一次…

25种代码坏味道总结+优化示例

前言什么样的代码是好代码呢&#xff1f;好的代码应该命名规范、可读性强、扩展性强、健壮性......而不好的代码又有哪些典型特征呢&#xff1f;这25种代码坏味道大家要注意啦1. Duplicated Code &#xff08;重复代码&#xff09;重复代码就是不同地点&#xff0c;有着相同的程…

滚动照片抽奖软件

CODE GitHub 源码 1、女友说很丑的一个软件 说个最近的事情&#xff0c;女友公司过年了要搞活动&#xff0c;需要个抽奖的环节&#xff0c;当时就问我能不能给做一个&#xff0c;正好我也没啥事儿&#xff0c;就在周末的时候用C#做了一个&#xff0c;虽然派上用场了&#xf…

Java即时类| 带示例的compareTo()方法

即时类compareTo()方法 (Instant Class compareTo() method) compareTo() method is available in java.time package. compareTo()方法在java.time包中可用。 compareTo() method is used to compare this Instant object to the given object. compareTo()方法用于将此Instan…

11个小技巧,玩转Spring!

前言最近有些读者私信我说希望后面多分享spring方面的文章&#xff0c;这样能够在实际工作中派上用场。正好我对spring源码有过一定的研究&#xff0c;并结合我这几年实际的工作经验&#xff0c;把spring中我认为不错的知识点总结一下&#xff0c;希望对您有所帮助。一 如何获取…

MFC中的几种播放声音的方法

一&#xff0e;播放声音文件的简单方法  在VC 中的多媒体动态连接库中提供了一组与音频设备有关的函数。利用这些函数可以方便地播放声音。最简单的播放声音方法就是直接调用VC中提供的声音播放函 数BOOL sndPlaySound ( LPCSTR lpszSound,UINT fuSound ); 或BOOL PlaySound(…

标志枚举的使用

标志枚举的使用大多是在标记多重状态&#xff0c;比如说文件的属性&#xff1a;只读&#xff0c;可写&#xff0c;隐藏&#xff0c;系统文件等相关属性&#xff0c;都对应相应的标志位&#xff0c;如果在C#中想实现自己的标志枚举&#xff0c;也是可以的&#xff0c;下文是亲身…

duration java_Java Duration类| 带示例的getUnits()方法

duration java持续时间类getUnits()方法 (Duration Class getUnits() method) getUnits() method is available in java.time package. getUnits()方法在java.time包中可用。 getUnits() method is used to get the List object that contains the units of seconds and Nanos …

synchronized 的超多干货!

synchronized 这个关键字的重要性不言而喻&#xff0c;几乎可以说是并发、多线程必须会问到的关键字了。synchronized 会涉及到锁、升级降级操作、锁的撤销、对象头等。所以理解 synchronized 非常重要&#xff0c;本篇文章就带你从 synchronized 的基本用法、再到 synchronize…

团队项目—第二阶段第三天

昨天&#xff1a;快捷键的设置已经实现了 今天&#xff1a;协助成员实现特色功能之一 问题&#xff1a;技术上遇到了困难&#xff0c;特色功能一直没太大的进展。网上相关资料不是那么多&#xff0c;我们无从下手。 有图有真相&#xff1a; 转载于:https://www.cnblogs.com/JJJ…

C# struct 装箱拆箱例子

值类型&#xff1a;拆箱、装箱 struct是值类型 struct和class的区别 类是引用类型&#xff0c;struct是值类型在托管堆上创建类的实例&#xff0c;在栈上创建struct实例类实例的赋值&#xff0c;赋的是引用地址&#xff0c;struct实例的赋值&#xff0c;赋的是值类作为参数类…