温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

ThreadLocal中内存溢出的原因有哪些

发布时间:2021-06-18 14:45:58 来源:亿速云 阅读:142 作者:Leah 栏目:web开发

这篇文章将为大家详细讲解有关ThreadLocal中内存溢出的原因有哪些,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

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

  • 使用锁(使用 synchronized 或 Lock);

  • 使用 ThreadLocal。

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

ThreadLocal中内存溢出的原因有哪些

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

ThreadLocal中内存溢出的原因有哪些

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

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

什么是内存溢出?

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

内存溢出代码演示

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

ThreadLocal中内存溢出的原因有哪些

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

ThreadLocal中内存溢出的原因有哪些

  • 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];     }          // 定义 ThreadLocal     private 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() {             @Override             public void run() {                 System.out.println("创建对象");                 // 创建对象(10M)                 MyTask myTask = new MyTask();                 // 存储 ThreadLocal                 taskThreadLocal.set(myTask);                 // 将对象设置为 null,表示此对象不在使用了                 myTask = null;             }         });     } }

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

ThreadLocal中内存溢出的原因有哪些

从上述图片可看出,当程序执行到第 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 中     else         createMap(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;             }             // 发现空位插入 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 中的值。

根据上面的内容,我们可以得出 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];     }      // 定义 ThreadLocal     private 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() {             @Override             public void run() {                 System.out.println("创建对象");                 try {                     // 创建对象(10M)                     MyTask myTask = new MyTask();                     // 存储 ThreadLocal                     taskThreadLocal.set(myTask);                     // 其他业务代码...                 } finally {                     // 释放内存                     taskThreadLocal.remove();                 }             }         });     } }

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

ThreadLocal中内存溢出的原因有哪些

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

remove的秘密

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

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

关于ThreadLocal中内存溢出的原因有哪些就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI