这篇文章将为大家详细讲解有关ThreadLocal中内存溢出的原因有哪些,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。
使用锁(使用 synchronized 或 Lock);
使用 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]; } // 定义 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; } }); } }
以上程序的执行结果如下:
从上述图片可看出,当程序执行到第 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 相关对象的关系图,如下所示:
也就是说它们之间的引用关系是这样的: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(); } } }); } }
以上程序的执行结果如下:
从上述结果可以看出我们只需要在 finally 中执行 ThreadLocal 的 remove 方法之后就不会在出现内存溢出的问题了。
那 remove 方法为什么会有这么大的魔力呢?我们打开 remove 的源码看一下:
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
关于ThreadLocal中内存溢出的原因有哪些就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。