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,一经查实,立即删除!

相关文章

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

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

湖南多校对抗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;它被包含在…

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

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

滚动照片抽奖软件

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

11个小技巧,玩转Spring!

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

synchronized 的超多干货!

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

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

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

不重启JVM,替换掉已经加载的类,偷天换日?

来源 | 美团技术博客在遥远的希艾斯星球爪哇国塞沃城中&#xff0c;两名年轻的程序员正在为一件事情苦恼&#xff0c;程序出问题了&#xff0c;一时看不出问题出在哪里&#xff0c;于是有了以下对话&#xff1a;“Debug一下吧。”“线上机器&#xff0c;没开Debug端口。”“看日…

[nodejs] 利用openshift 撰寫應用喔

2019独角兽企业重金招聘Python工程师标准>>> 朋友某一天告訴我,可以利用openshift來架站,因為他架了幾個nodejs應用放在上面,我也來利用這個平台架看看,似乎因為英文不太行,搞很久啊!! 先來架一個看看,不過架好之後,可以有三個應用,每個應用有1G的空間,用完就沒啦~~…

详解4种经典的限流算法

最近&#xff0c;我们的业务系统引入了Guava的RateLimiter限流组件&#xff0c;它是基于令牌桶算法实现的,而令牌桶是非常经典的限流算法。本文将跟大家一起学习几种经典的限流算法。限流是什么?维基百科的概念如下&#xff1a;In computer networks, rate limiting is used t…

css clearfix_如何使用CSS清除浮点数(clearfix)?

css clearfixIntroduction: 介绍&#xff1a; Dealing with various elements on a website or web page can sometimes prove to create many problems hence one should be aware of many properties, tricks or ways to cope with those problems. We do not want our webs…

将你的Windows,快速打造成Docker工作站!

手里的macbook因为键盘问题返厂维修了&#xff0c;只好抱起了久违的Windows。首先面临的&#xff0c;就是Docker问题。docker好用&#xff0c;但安装麻烦&#xff0c;用起来也命令繁多。一个小白&#xff0c;如何打造舒适的docker环境&#xff0c;是一个非常有挑战的问题。本文…

漫画:什么是JVM的垃圾回收?

————— 第二天 —————————————————下面我们一起来研究这三个问题。问题1&#xff1a;哪些是需要回收的&#xff1f;首先我们需要知道如何哪些垃圾需要回收&#xff1f;判断对象是否需要回收有两种算法。一种是引用计数算法、一种是可达性分析算法。引用计…

48张图|手摸手教你性能监控、压测和调优

本文主要内容一、何为压力测试1.1、 大白话解释性能压测是什么&#xff1a;就是考察当前软件和硬件环境下&#xff0c;系统所能承受的最大负荷&#xff0c;并帮助找出系统的瓶颈所在。性能压测的目的&#xff1a;为了系统在线上的处理能力和稳定性维持在一个标准范围内&#xf…

Java生成随机数的4种方式,以后就用它了!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在 Java 中&#xff0c;生成随机数的场景有很多&#xff0c;所以本文我们就来盘点一下 4 种生成随机数的方式&#xff0c;以…

Everything是如何搜索的

写在前面 使用了Everything之后&#xff0c;一直对他的搜索速度感兴趣&#xff0c;在网上也看了很多对其原理的揭秘&#xff0c;终于有空找了个源码研究了一下&#xff0c;原理就是对NTFS的USN特性进行使用。 原理 详细解释我参照别人家的博客来一段&#xff1a; 当扇区的文…

漫话:如何给女朋友解释String对象是不可变的?

String的不变性String在Java中特别常用&#xff0c;相信很多人都看过他的源码&#xff0c;在JDK中&#xff0c;关于String的类声明是这样的&#xff1a;public final class String implements java.io.Serializable, Comparable<String>, CharSequence { }可以看到&#…

XenServer 6.5实战系列之十一:Install Update For XenServer 6.5

为了保证XenServer主机的安全及功能的更新&#xff0c;在企业环境中我们需要定期的到Citrix官网或通过XenCenter进行下载和更新。今天我们会从在线和离线两种不同的方法进行Update的安装。更新补丁之前请务必阅读对应Update的相关资料、注意事项和做好备份。1. 离线安装更新在…