当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2023-09-22 20:09:50 440观看
导读前言ThreadLocal 翻译成中文是线程本地变量的意思,也就是说它是线程中的私有变量,每个线程只能操作自己的私有变量,所以不会造成线程不安全的问题。所谓的线程不安全是指,多个线程在同一时刻对同一个全局变量做写操作时(读

前言

ThreadLocal 翻译成中文是线程本地变量的意思,也就是说它是线程中的私有变量,每个线程只能操作自己的私有变量,所以不会造成线程不安全的问题。Zlp28资讯网——每日最新资讯28at.com

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

在 Java 语言中解决线程不安全的问题通常有两种手段:Zlp28资讯网——每日最新资讯28at.com

  1. 使用锁(使用 synchronized 或 Lock);
  2. 使用 ThreadLocal。

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

图片图片Zlp28资讯网——每日最新资讯28at.com

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

图片图片Zlp28资讯网——每日最新资讯28at.com

PS:创建 SimpleDateFormat 也会消耗一定的时间和空间,如果线程复用 SimpleDateFormat 的频率比较高的情况下,使用 ThreadLocal 的优势比较大,反之则可以考虑使用锁。Zlp28资讯网——每日最新资讯28at.com

然而,在我们使用 ThreadLocal 的过程中,很容易就会出现内存溢出的问题,如下面的这个事例。Zlp28资讯网——每日最新资讯28at.com

什么是内存溢出?

内存溢出(Memory Overflow),指的是在程序运行过程中,申请的内存资源不再被使用,但没有被正确释放,导致占用的内存不断增加,最终耗尽系统的可用内存。当程序尝试分配更多的内存空间时,由于内存不足,会抛出 OutOfMemoryError 异常,导致程序终止或崩溃的现象就叫做内存溢出。Zlp28资讯网——每日最新资讯28at.com

内存溢出代码演示

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

图片图片Zlp28资讯网——每日最新资讯28at.com

设置后的最终效果这样的:Zlp28资讯网——每日最新资讯28at.com

图片图片Zlp28资讯网——每日最新资讯28at.com


Zlp28资讯网——每日最新资讯28at.com

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

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

import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;publicclass ThreadLocalOOMExample {        /**     * 定义一个 10m 大的类     */    staticclass MyTask {        // 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)        privatebyte[] bytes = newbyte[10 * 1024 * 1024];    }        // 定义 ThreadLocal    privatestatic 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() {            @Override            public void run() {                System.out.println("创建对象");                // 创建对象(10M)                MyTask myTask = new MyTask();                // 存储 ThreadLocal                taskThreadLocal.set(myTask);                // 将对象设置为 null,表示此对象不在使用了                myTask = null;            }        });    }}

以上程序的执行结果如下:Zlp28资讯网——每日最新资讯28at.com

图片图片Zlp28资讯网——每日最新资讯28at.com

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

原因分析

内存溢出的问题和解决方案比较简单,重点在于“原因分析”,我们要通过内存溢出的问题搞清楚,为什么 ThreadLocal 会这样?是什么原因导致了内存溢出?Zlp28资讯网——每日最新资讯28at.com

要搞清楚这个问题(内存溢出的问题),我们需要从 ThreadLocal 源码入手,所以我们首先打开 set 方法的源码(在示例中使用到了 set 方法),如下所示:Zlp28资讯网——每日最新资讯28at.com

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

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

staticclass 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;            }            // 发现空位插入 value            if (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 中的值。Zlp28资讯网——每日最新资讯28at.com

根据上面的内容,我们可以得出 ThreadLocal 相关对象的关系图,如下所示:Zlp28资讯网——每日最新资讯28at.com

图片图片Zlp28资讯网——每日最新资讯28at.com

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

解决方案

ThreadLocal 内存溢出的解决方案很简单,我们只需要在使用完 ThreadLocal 之后,执行 remove 方法就可以避免内存溢出问题的发生了,比如以下代码:Zlp28资讯网——每日最新资讯28at.com

import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;publicclass App {    /**     * 定义一个 10m 大的类     */    staticclass MyTask {        // 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)        privatebyte[] bytes = newbyte[10 * 1024 * 1024];    }    // 定义 ThreadLocal    privatestatic 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() {            @Override            public void run() {                System.out.println("创建对象");                try {                    // 创建对象(10M)                    MyTask myTask = new MyTask();                    // 存储 ThreadLocal                    taskThreadLocal.set(myTask);                    // 其他业务代码...                } finally {                    // 释放内存                    taskThreadLocal.remove();                }            }        });    }}

以上程序的执行结果如下:Zlp28资讯网——每日最新资讯28at.com

图片图片Zlp28资讯网——每日最新资讯28at.com

从上述结果可以看出我们只需要在 finally 中执行 ThreadLocal 的 remove 方法之后就不会在出现内存溢出的问题了。Zlp28资讯网——每日最新资讯28at.com

remove的秘密

那 remove 方法为什么会有这么大的魔力呢?我们打开 remove 的源码看一下:Zlp28资讯网——每日最新资讯28at.com

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

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

小结

本文我们使用代码的方式演示了 ThreadLocal 内存溢出的问题,严格来讲内存溢出并不是 ThreadLocal 的问题,而是因为没有正确使用 ThreadLocal 所带来的问题。想要避免 ThreadLocal 内存溢出的问题,只需要在使用完 ThreadLocal 后调用 remove 方法即可。Zlp28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-11177-0.htmlThreadLocal内存溢出演示和原因分析!

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: Python 作为 AI 和 ML 开发语言的优势

下一篇: 为什么建议用const,enum,inline 替换 #define?

标签:
  • 热门焦点
  • 7月安卓手机性价比榜:努比亚+红魔两款新机入榜

    7月登场的新机有努比亚Z50S Pro和红魔8S Pro,除了三星之外目前唯二的两款搭载超频版骁龙8Gen2处理器的产品,而且努比亚和红魔也一贯有着不错的性价比,所以在本次的性价比榜单
  • 掘力计划第 20 期:Flutter 混合开发的混乱之治

    在掘力计划系列活动第20场,《Flutter 开发实战详解》作者,掘金优秀作者,Github GSY 系列目负责人恋猫的小郭分享了Flutter 混合开发的混乱之治。Flutter 基于自研的 Skia 引擎
  • Flowable工作流引擎的科普与实践

    一.引言当我们在日常工作和业务中需要进行各种审批流程时,可能会面临一系列技术和业务上的挑战。手动处理这些审批流程可能会导致开发成本的增加以及业务复杂度的上升。在这
  • 在线图片编辑器,支持PSD解析、AI抠图等

    自从我上次分享一个人开发仿造稿定设计的图片编辑器到现在,不知不觉已过去一年时间了,期间我经历了裁员失业、面试找工作碰壁,寒冬下一直没有很好地履行计划.....这些就放在日
  • 腾讯盖楼,字节拆墙

    来源 | 光子星球撰文 | 吴坤谚编辑 | 吴先之&ldquo;想重温暴刷深渊、30+技能搭配暴搓到爽的游戏体验吗?一起上晶核,即刻暴打!&rdquo;曾凭借直播腾讯旗下代理格斗游戏《DNF》一
  • 签约井川里予、何丹彤,单视频点赞近千万,MCN黑马永恒文希快速崛起!

    来源:视听观察永恒文希传媒作为一家MCN公司,说起它的名字来,可能大家会觉得有点儿陌生,但是说出来下面一串的名字之后,或许大家就会感到震惊,原来这么多网红,都签约这家公司了。根
  • 阿里大调整

    来源:产品刘有媒体报道称,近期淘宝天猫集团启动了近年来最大的人力制度改革,涉及员工绩效、层级体系等多个核心事项,目前已形成一个初步的&ldquo;征求意见版&rdquo;:1、取消P序列
  • 苹果公司要求三星和LG Display生产「无边框」OLED iPhone显示屏

    据 The Elec 报道,苹果已要求其供应商为未来的 iPhone 型号开发「无边框」OLED 显示面板。苹果显然已要求三星和 LG Display 开发新的 OLED 显示面
  • OPPO K11采用全方位护眼屏:三大护眼能力减轻视觉疲劳

    日前OPPO官方宣布,全新的OPPO K11将于7月25日正式发布,将主打旗舰影像,和同档位竞品相比,其最大的卖点就是将配备索尼IMX890主摄,堪称是2000档位影像表
Top