作者 | 王磊

来源 | 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自白:限流器用我就对了!

ThreadLocal内存溢出代码演示和原因分析!相关推荐

  1. 【java】ThreadLocal 内存泄漏 代码演示 实例演示

    1.概述 转载:ThreadLocal 内存泄漏 代码演示 实例演示 首先看文章:ThreadLocal内存泄露原因分析 相关文章: [高并发]ThreadLocal.InheritableThrea ...

  2. JVM 调优实战--内存溢出的定位和MAT分析

    目录 内存溢出的定位和分析 模拟内存溢出代码 MAT分析 内存溢出的定位和分析 模拟内存溢出代码 添加运行参数: ①-Xms8m:初始堆内存大小为8M: ②-Xmx8m:最大堆内存大小为8M: ③He ...

  3. java 内存 溢出_java内存溢出的几种原因和解决办法是什么?

    java内存溢出的几种原因和解决办法是什么? java内存溢出的几种原因和解决办法是: 第一类内存溢出,也是大家认为最多,第一反应认为是的内存溢出,就是堆栈溢出: 那什么样的情况就是堆栈溢出呢?当你看 ...

  4. 内存溢出的几种原因和解决办法

    对于JVM的内存写过的文章已经有点多了,而且有点烂了,不过说那么多大多数在解决OOM的情况,于此,本文就只阐述这个内容,携带一些分析和理解和部分扩展内容,也就是JVM宕机中的一些问题,OK,下面说下O ...

  5. Java 内存溢出(一)原因、复现、排查

    目录 一.内存溢出原因 二.内存溢出实例 1.堆溢出 2.虚拟机栈和本地方法栈溢出 3.方法区和运行时常量池溢出 4.本机直接内存溢出 三.内存溢出排查 内存溢出: 是指应用系统中存在无法回收的内存或 ...

  6. iOS 内存泄漏排查方法及原因分析

    级别: ★★☆☆☆ 标签:「iOS」「内存泄漏排查」「Leaks工具」 作者: MrLiuQ 审校: QiShare团队 本文将从以下两个层面解决iOS内存泄漏问题: 内存泄漏排查方法(工具) 内存泄 ...

  7. 内存溢出的几种原因和解决办法是什么?

    内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存. 引起内存溢出的原因有很多种,常见的有以下几种: 内存中加载的数据量过于庞大,如一次从 ...

  8. 内存溢出常见报错原因

    StackOverFlowError :方法地柜调用,栈内存溢出. OutOfMemoryError:Java heap space 大量对象创建撑爆堆区,堆内存溢出. OutOfMemoryErro ...

  9. 什么情况下java会出现堆溢出_【Java面试题第三期】JVM中哪些地方会出现内存溢出?出现的原因是什么?...

    内存溢出(Out Of Memory)经常简称为OOM,在jvm中主要分为方法区.堆.栈.本地方法栈.程序计数器这几部分,其中程序计数器是唯一不会出现OOM的,也就是说其他区域都会出现OOM.下面来分 ...

最新文章

  1. if(window.event) e = window.event
  2. python中国余数定理_Python实现的中国剩余定理算法示例
  3. 成功解决AttributeError: module 'tensorflow' has no attribute 'histogram_summary'
  4. matplotlib 画多条折线图且x轴下标非数值
  5. mac docker搭建开发环境
  6. 前台等待事件 oracle,Oracle等待事件之buffer busy waits
  7. 【软件质量】CMM与CMMI
  8. 报告PPT(123页):Python编程基础精要
  9. android java pipe_Java-使用Dagger 2进行Android单元测试
  10. 穷人的孩子真的早当家吗?
  11. 收藏!MySQL 面试必须掌握的 8 个知识点!
  12. 大数据系列(hadoop) 集群环境搭建二
  13. Android 开发实战
  14. 横向时间轴(进度条)
  15. android 编程w3c,w3cschool手机版app下载-w3cschool-编程学院 安卓版v3.4.73-PC6安卓网
  16. 在Qt工程中调用GmSSL
  17. android桌面 vulkan,Vulkan 设计指南
  18. C64X EDMA优先级及优先级队列
  19. YesPlayMusic 0.4.0中文版:一款mac用户必备的网易云音乐客户端
  20. 新神魔大陆服务器现在在维护吗,新神魔大陆1月22日合服维护公告

热门文章

  1. 超过响应缓冲区限制_Nginx如何限制并发连接数和连接请求数?
  2. 【Java从入门到头秃专栏 】(一)学在Java语法之前
  3. ubuntu下安装jdk
  4. Oracle关联查询-数据类型不一致问题 ORA-01722: 无效数字
  5. 用Emit技术替代反射
  6. Redis Sentinel 模拟故障迁移
  7. 蓝牙基础知识进阶——Physical channel
  8. Centos7: 配置IO调度
  9. Spark(二): 内存管理
  10. 接口测试从零开始系列_mock技术使用