前言

大家对于ThreadLocal这一个都应该听说过的吧,不知道大家对于这个掌握的怎么样了已经

这不,我那爱学习的表妹不知道又从哪里听来了这个技术点,回家就得意洋洋的给我说,表哥,我今天又学会了一个技术点ThreadLocal

哦,不错啊

你你这态度,好像不太信的样子啊,表妹咬牙切齿的说着

没没没,我信。我表妹那么聪明伶俐,肯定会

不行,你这态度太敷衍了,不信我给你讲一遍

得,你也先别给我讲了,你把你的Mac拿过来,我给你写个东西

接过她的Mac,我三下五除二给她写了一个小例子

public class ThreadPool {private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 10, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000; ++i) {poolExecutor.execute(new Runnable() {@Overridepublic void run() {ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();threadLocal.set(new BigObject());// 其他业务代码}});Thread.sleep(1000);}}static class BigObject {// 100Mprivate byte[] bytes = new byte[100 * 1024 * 1024];}
}

你先看看这段代码,给我说说你的理解

表妹眉头一皱,你这是侮辱我的智商吗,这不就是创建了一个线程池,然后使用for循环增加线程,往线程池里面提交一千个任务吗

这也没啥问题啊,每个任务会向ThreadLocal变量里面塞一个大对象,然后执行其他业务逻辑

总之,看着没啥大毛病,这就是表妹的结论

如果你觉得这段代码没啥问题,那看来你对ThreadLocal学的还是不够彻底啊

代码分析

来,我来给你透彻的说一遍,包教包会

先分析一下上面的代码

  • 创建一个核心线程数和最大线程数都为10的线程池,保证线程池里一直会有10个线程在运行。

  • 使用for循环向线程池中提交了100个任务。

  • 定义了一个ThreadLocal类型的变量,Value类型是大对象。

  • 每个任务会向threadLocal变量里塞一个大对象,然后执行其他业务逻辑。

  • 由于没有调用线程池的shutdown方法,线程池里的线程还是会在运行。

说个结论

上面的代码会造成内存泄漏,会让服务的内存一直很高,即使GC之后也不会降低太多,这不是我们想要的结果

 

ThreadLocal存储模型

在ThreadLocal的内部有一个静态内部类ThreadLocalMap,这个才是真正存储对象的Map,我们平时使用的set存储的值实际上是存储到这里面的

static class ThreadLocalMap {// 定义一个table数组,存储多个threadLocal对象及其value值private Entry[] table;ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}// 定义一个Entry类,key是一个弱引用的ThreadLocal对象// value是任意对象static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 省略其他
}

我们重点来看set正如上面代码所示,ThreadLocalMap的内部实际是一个Entry数组,而这个Entry对象就是Key和Value组成

重点来了,这个Key就是ThreadLocal实例本身,这个Value就是我们要存储的真实的数据

大家看到这,是不是觉得很熟悉,没错,这个ThreadLocalMap就是一个Map而已,这个Map和普通的Map有两点不同之处

1、Key、Value的存储内容的不同

2、ThreadLocalMap的Key是一个弱引用类型

其实吧,第一点也不算是不同,只是这里存储的Key有点出乎我们的意料,这里重点的重点其实是这个第二点,也就是这个弱引用类型,大家先记着,下面说

我们先来看一下ThreadLocal的get和set方法来验证一下我的说法

ThreadLocal的set方法

public class ThreadLocal<T> {public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}// 省略其他方法
}

set的逻辑其实也是很简单的,获取当前线程的ThreadLocalMap,然后就直接往map里面添加Key和Value,而这个Key就是this,这个this就是ThreadLocal实例本身了

value就是我们要存储的数据

这里需要注意一下,map的获取是需要从Thread类对象里面取,看一下Thread类的定义。

public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;//省略其他
}

ThreadLocal的get方法

class ThreadLocal<T> {public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}return setInitialValue();}
}

弱引用获取当前线程的ThreadLocalMap实例,如果不为空,直接用当前ThreadLocal的实例来作为Key获取Value即可

如果ThreadLocalMap为空,或者根据当前的ThreadLocal实例获取到的Value为空,则执行setInitialValue()

而setInitialValue的内部实现就是如果Map不为空,就设置键值对,为空,则创建Map

 

ThreadLocal的内部关系

这个图画的很清晰了

每个Thread线程会有一个threadlocals,这是一个ThreadLocalMap对象

通过这个对象,可以存储线程的私有变量,就是通过ThreadLocal的set和get来操作

ThreadLocal本身不是一个容器,本身不存储任何数据,实际存储数据的对象是ThreadLocalMap对象,操作的过程就类似于Map的put和get

这个ThreadLocalMap对象就是负责ThreadLocal真实存储数据的对象,内部的存储结构是Entry数组,这个Entry就是存储Key和Value对象

Key就是ThreadLocal实例本身,而Value就是我们要存储的真实数据,而我们也从上面的源码中看到了,存和取就是根据ThreadLocal实例来操作的

ThreadLocal内存模型

图中左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。

  • 线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef。

  • 当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。

  • Map实例化之后,也就拿到了该ThreadLocalMap的句柄,那么就可以将当前ThreadLocal对象作为key,进行存取操作。

  • 图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用。

四种引用

强引用,一直活着

类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。

弱引用,回收就会死亡

被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

软引用,有一次活的机会

软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

虚引用,也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。

一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

 

ThreadLocal中的弱引用和内存泄漏

static class ThreadLocalMap {// 定义一个Entry类,key是一个弱引用的ThreadLocal对象// value是任意对象static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
}

我们先来看下Entry的实现,Key会被保存到弱引用WeakReference中

这里的Key作为弱引用是关键,我们分两种情况来讨论

Key作为强引用的时候

引用ThreadLocal的对象被回收了,但是ThreadLocalMap还有ThreadLocal的强引用,所以如果没有进行手动删除的话,ThreadLocal是不会被GC回收的,也就是会导致Entry的内存泄露

一句话,强引用的时候需要手动删除才会释放内存

Key作为弱引用的时候

引用ThreadLocal的对象被回收了之后,由于ThreadLocalMap持有的是ThreadLocal的弱引用,即使不会手动删除这个ThreadLocal,这个ThreadLocal也会被回收

前提是该对象只被弱引用所关联,别的强引用关联不到!

而Value则是在下一次调用get、set、remove的时候进行清除,才会被GC自动回收

一句话,弱引用是多一层屏障,无外部强引用的时候,弱引用ThreadLocal会被GC回收,但是该ThreadLocal对应的Value只有执行set、get和remove的时候才会被清除

比较这两种情况

由于ThreadLocalMap的生命周期是和Thread一样的,因为它是Thread内部实现的,如果没有手动删除对应的key,都会导致内存泄漏

而ThreadLocal使用弱引用,会多了一层保障,ThreadLocal在被清理之后,也就是Map中的key会变成null,在使用对应value的时候就会将这个value进行清除

但是!但是!但是!

使用弱引用并不代表不需要考虑内存泄漏,只是多了一层屏障而已!

造成内存泄漏的根源就是:ThreadLocalMap和Thread的生命周期一样长,如果没有手动删除对应key,就会导致相应的value不能及时得到清除,造成内存泄漏

我们在线上使用最多的就是线程池了,这样问题就大了

你想啊,线程池里面有10个活跃线程,线程一直在运行,不会停止,每次线程直接拿到过来用,然后用完之后会再次放到线程池中,此时线程并不会停止

也就是说这些线程的每一次使用都有可能产生新的ThreadLocal,而我们使用完对应的ThreadLocal之后,如果不去手动执行remove删除相应的key,就会导致ThreadLocalMap中的Entry一直在增加,并且内存是永远得不到释放

这本身就是一个很恐怖的事情,再要是放到ThreadLocal中的对象还是超大对象,那后果不堪设想

 

如何避免内存泄漏

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

答案就是:每次使用完ThreadLocal,建议调用它的remove()方法,清除数据。

另外需要强调的是并不是所有使用ThreadLocal的地方,都要在最后remove(),因为他们的生命周期可能是需要和项目的生存周期一样长的,所以要进行恰当的选择,以免出现业务逻辑错误!

 

ThreadLocal的应用场景

场景1:

ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

场景2:

ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

举个具体的使用例子

比如可以用于保存线程不安全的工具类,典型的需要使用的类就是 SimpleDateFormat。

在这种情况下,每个Thread内都有自己的实例副本,且该副本只能由当前Thread访问到并使用,相当于每个线程内部的本地变量,这也是ThreadLocal命名的含义。因为每个线程独享副本,而不是公用的,所以不存在多线程间共享的问题。

这种线程不安全的工具类如果需要在很多的线程中同时使用的话,任务数量巨大的情况下,也就是需要线程数巨多的情况下,这个不安全我们就需要让它变得安全

比如使用Synchronized锁,这样可以解决,但是Synchronized会让线程进入一个排队的状态,大大降低整体的工作效率

我们在线上一般使用线程池,ThreadLocal再合适不过了,ThreadLocal给每个线程维护一个自己的simpleDateFormat对象,这个对象在线程之间是独立的,互相没有关系的。这也就避免了线程安全问题。与此同时,simpleDateFormat对象还不会创造过多,线程池有多少个线程,所以需要多少个对象即可。

再说一个形象的场景

每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

这其实就是类似于一种责任链的模式

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

线上系统因为一个ThreadLocal直接内存飙升相关推荐

  1. 线上系统的JVM内存是否设置的越大越好?

    "线上系统的JVM内存是否设置的越大越好?"乍一听到这个问题,我第一个反应就是"NO",但是,具体为什么不行,又说不出什么所以然来. 其实,说一个东西不行,我们 ...

  2. 重大事故!线上系统频繁卡死,凶手竟然是 Full GC ?

    每日鸡汤,好喝 01 案发现场 通常来说,一个系统在上线之前应该经过多轮的调试,在测试服务器上稳定的运行过一段时间.我们知道 Full GC 会导致 Stop The World 情况的出现,严重影响 ...

  3. 程序员搞事!动手实战优化自己公司线上系统JVM,结果。。。

    Java性能调优都是老生常谈的问题,特别当"糙快猛"的开发模式大行其道时,随着系统访问量的增加.代码的臃肿,各种性能问题便会层出不穷. 比如,下面这些典型的性能问题,你肯定或多或少 ...

  4. jvm性能调优实战 - 30使用jmap和jhat摸清线上系统的对象分布

    文章目录 Pre 使用jmap了解系统运行时的内存区域 jmap -heap PID 使用jmap了解系统运行时的对象分布 jmap -histo PID 使用jmap生成堆内存转储快照 使用jhat ...

  5. 阿里Java诊断工具 arthas - 监测线上系统的运行信息、排查程序运行缓慢等问题

    一.arthas 上篇文章我们讲解了使用arthas在线上环境排查定位内存占用过大.cpu使用率过高等问题,本篇文章继续使用arthas监测线上系统的运行信息,以及排查程序运行缓慢等问题. 下面是上篇 ...

  6. 面试两连击:线上JVM GC问题和OOM内存溢出的解决方案,聊聊呗!

    点击上方石杉的架构笔记,右上角选择"设为星标" 每日早8点半,技术文章准时送上 公众号后台回复"学习",获取作者独家秘制精品资料 往期文章 BAT 面试官是如何 ...

  7. 如何更快的查找线上系统问题--多次重大线上事故复盘

    以前我以为,线上系统的问题,只需要好好检查代码即可找出原因,可是工作后发现,现实并非如此,往往线上系统的问题来源于信息不对称.这种信息不对称体现在团队成员之间没有好好沟通,了解彼此对系统的改动,以及跨 ...

  8. c语言学生综合测评系统_综合测评线上系统帮助文档

    综合测评线上系统填写Q&A *前言:帮助文档的目的是帮助同学们高效地完成线上系统综测填写,这个过程其实并不复杂,大家一步一步来就可以,每一步有问题先看帮助文档.后续如果有统一需要改动或者说明的 ...

  9. 质量基础设施一站式服务平台建设,NQI线上系统开发方案

    质量基础设施一站式服务平台建设,NQI线上系统开发方案 质量基础设施一站式服务,即通过有机融合计量.标准.认证认可.检验检测等要素资源,面向产业.区域.企业特别是中小微企业和民营企业提供的全链条.全方 ...

最新文章

  1. Nginx开启gzip压缩解决react打包文件过大
  2. ASP:当 request.cookies 发生 Microsoft VBScript 运行时错误 (0x800A000D) 类型不匹配: '[string:...
  3. How to track an installation through client log-fi
  4. python数字求和输入完第一个数没反应_Python 数字求和
  5. Object关于属性property的静态方法
  6. 信号量释放和等待函数sem_post()和sem_wait()
  7. 网际控制报文协议ICMP(Internet Control Message Protocol)(详解)
  8. Python小白的数学建模课-B2. 新冠疫情 SI模型
  9. 程序开发,面试恐惧症_如何克服恐惧并停止讨厌的工作面试
  10. Nginx之配置后端服务器组
  11. react dispatch_梳理下redux、mobx 在react的应用
  12. HTTP协议详细总结
  13. Python解析JSON数据的基本方法
  14. 离散数学第7章欧拉图,哈密顿图
  15. pandas统计个数
  16. java-学生管理系统源代码
  17. mac 谷歌浏览器 跨域访问
  18. LEDs状态灯任务(线程)设计(基于RTOS)
  19. dp训练第27题 vijos1153 猫狗大战 背包
  20. CentOS8-1905 本地dnf源挂载

热门文章

  1. 词云-vue-wordcloud组件封装
  2. python访问数据库统一方法_Python 3.x 连接数据库(pymysql 方式)
  3. django使用mysql原始语句,Django中使用mysql数据库并使用原生sql语句操作
  4. 云南省行政村谷歌图层_云南省基本农田划定工作实施细则
  5. numpy的常用函数 不断更新
  6. Windows 服务程序编写
  7. 用Java编写的密码翻译问题
  8. Nmap源码分析(整体架构)
  9. QT5_数据类型转化
  10. Z-Stack Home Developer's Guide—4.Using the sample applications as base for new applications 中文翻译