很多时候,当我们需要存储线程私有变量或者要实现线程安全的变量时或者想减少线程资源竞争的时候,可以使用ThreadLocal来为每个线程存储对应的私有变量。但是,如果你使用不当,会有可能造成严重的问题,最容易出现的就是内存泄漏。今天以一个案例分析出发,给大家介绍一下TheadLocal的原理及使用TheadLocal时注意的事项。

案例介绍

出于公司代码的隐私,这里将相关的地方简单写成一个demo作为实际场景的还原。相关代码如下:

public class ThreadPoolDemo {private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());public static void main(String[] args) throws InterruptedException {System.out.println("执行开始!!");for (int i = 0; i < 200; ++i) {poolExecutor.execute(new Runnable() {@Overridepublic void run() {ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();threadLocal.set(new BigObject());// 其他业务代码//最终清除//threadLocal.remove();}});Thread.sleep(1000);}System.out.println("执行完毕!!");}static class BigObject {// 200Mprivate byte[] bytes = new byte[200 * 1024 * 1024];}
}

对应的流程是这样的:

  1. 首先创建一个核心线程数和最大线程数都为5的线程池;
  2. 利用线程池执行200个任务。

别看流程比较简单,但是需要注意的地方还是很多的。这不,正因为没有注意,代码上到生产环境以后导致服务器内存飙升,经过GC以后依然占据很多内存,最终导致内存泄露,报错java.lang.OutOfMemoryError: Java heap space。

那么到底是什么原因,导致服务GC以后内存还高居不下呢?在正式分析原因之前,我们先来看一下线程在执行过程使用TheadLocal存储变量的真实面目。

TheadLocal本质

对于存储或者获取,当然是离不开set/get方法。ThreadLocal类提供set/get方法存储和获取value值。但实际上ThreadLocal类并不存储value值,真正存储是靠ThreadLocalMap这个类,ThreadLocalMap是ThreadLocal的一个静态内部类,它的key是ThreadLocal实例对象,value是任意Object对象。看一下ThreadLocalMap定义的源码就很清楚了:

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;}}// 省略其他
}

TheadLocal类的set方法

可能到这里有的人比较迷惑,说的是TheadLocal,怎么又说到ThreadLocalMap呢?他们的关系到底是什么样子的呢?别急,我们来看看TheadLocal类的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);}// 省略其他方法
}

它的逻辑比较简单,获取当前线程的ThreadLocalMap,然后往map里添加KV,K是当前ThreadLocal实例,V是我们传入的value。我们看一下set方法的前两行代码:

 Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);

发现map的获取是从Thread类对象里面取的ThreadLocalMap对象,然后我们看一下Thread类的定义:

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

在Thread类中维护了一个ThreadLocalMap的变量引用。所以总结起来ThreadLocal的set方法的逻辑是这样的:

  1. 获取当前线程的ThreadLocalMap实例;
  2. 如果ThreadLocalMap为空,以当前ThreadLocal实例为key,传入的值作为value存储在ThreadLocalMap中;
  3. 如果ThreadLocalMap不为空,则将以当前ThreadLocal实例为key的对应的map的值更改为传入的value。

TheadLocal类的get方法

看了以上的set方法,get方法相对而言就更加简单,get获取当前线程的对应的私有变量,是之前set或者通过initialValue的值。源码如下:

public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}

通过源码我们总结ThreadLocal的set方法的逻辑是这样的:

  1. 获取当前线程的ThreadLocalMap实例;
  2. 如果ThreadLocalMap不为空,以当前ThreadLocal实例为key获取value;
  3. 如果ThreadLocalMap为空或者根据当前ThreadLocal实例获取的value为空,则执行setInitialValue();

可能看到这里有的人还是不太明白这里面的关系,Thread,ThreadLocal,ThreadLocalMap,Entry等等名词太多了,我们在这里再做一个小的总结:

  • 每个线程是一个Thread实例,其内部维护一个threadLocals的实例成员,其类型是ThreadLocal.ThreadLocalMap;
  • 通过实例化ThreadLocal实例,我们可以对当前运行的线程设置一些线程私有的变量,通过调用ThreadLocal的set和get方法存取;
  • ThreadLocal本身并不是一个容器,我们存取的value实际上存储在ThreadLocalMap中,ThreadLocal只是作为TheadLocalMap的key;
  • 每个线程实例都对应一个TheadLocalMap实例,我们可以在同一个线程里实例化很多个ThreadLocal来存储很多种类型的值,这些ThreadLocal实例分别作为key,对应各自的value,最终存储在Entry table数组中;
  • 当调用ThreadLocal的set/get进行赋值/取值操作时,首先获取当前线程的ThreadLocalMap实例,然后就像操作一个普通的map一样,进行put和get。

ThreadLocal内存模型

了解了ThreadLocal的基本操作原理,我们接下来看一下在使用ThreadLocal进行数据存储的时候的内存模型是什么样子的。

如上图所示,左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。我们简单描述一下上图中的流程:

  1. 线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef;
  2. 当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化;
  3. Map实例化之后,也就拿到了该ThreadLocalMap的句柄,那么就可以将当前ThreadLocal对象作为key,进行存取操作。其中图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用。

ThreadLocal实例的引用是个弱引用,这个在源码里面就体现了:

这里简单介绍几种引用:

引用类型 描述
强引用 类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
软引用 软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
弱引用 被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
虚引用 最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

泄露原理剖析

最后有了上面的理论,我们就能很好的解析内存泄漏的原因了:

  • 使用ThreadLocal存储对象,理论上引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
  • 但是,如果没有调用set、get、remove方法,由于ThreadLocalMap的生命周期跟Thread一样长,如果自动回收同时也没有手动删除对应key,这样会导致这些value没有使用但是越堆积越多,内存越占越多,最终导致内存泄漏。
  • 代码中给threadLocal赋值了一个大的对象,但是执行完业务逻辑后没有调用remove方法,最后导致线程池中10个线程的threadLocals变量中包含的大对象没有被释放掉,出现了内存泄露。

我们在代码中加完threadLocal.remove(),即将下面的注释放开以后,

程序正常执行完毕:

基于以上分析,我们在使用完ThreadLocal以后,建议调用它的remove()方法,清除数据,及时释放内存空间。当然并不是所有使用ThreadLocal的地方,都要在最后remove(),还是得根据你实际实际使用的场景进行取舍,比如如果定义的ThreadLocal的生命周期可能是需要和项目的生存周期一样长的,如果提前清除掉,则可能会出现业务逻辑错误。所以在以后使用ThreadLocal时,大家要注意哦!

想看更多文章,请点击此处

Java的TheadLocal使用相关推荐

  1. JavaWeb-新版

    JavaWeb 参考文章: https://heavy_code_industry.gitee.io/code_heavy_industry/pro001-javaweb/lecture/ 01.We ...

  2. Java TheadLocal

    原文链接 作者:Jakob Jenkov   查看全部文章 Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作.因此,尽管有两个线程同时执行一段相同的代码,而且这段代码 ...

  3. 内存泄露的原因找到了,罪魁祸首居然是 Java TheadLocal

    作者 | 雷架 来源 | 爱笑的架构师(ID:DancingOnYourCode) ThreadLocal使用不规范,师傅两行泪 组内来了一个实习生,看这小伙子春光满面.精神抖擞.头发微少,我心头一喜 ...

  4. 《Java并发性和多线程介绍》-Java TheadLocal

    原文链接 作者:Jakob Jenkov   查看全部文章 Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作.因此,尽管有两个线程同时执行一段相同的代码,而且这段代码 ...

  5. java.lang.ThreadLocal实现原理和源码分析

    java.lang.ThreadLocal实现原理和源码分析 1.ThreadLocal的原理:为每一个线程维护变量的副本.某个线程修改的只是自己的副本. 2.ThreadLocal是如何做到把变量变 ...

  6. Java中的ThreadLocal的使用--学习笔记

    ThreadLocal直译为"线程本地"或"本地线程",如果你真的这么认为,那就错了!其实它就是一个容器,用于存放线程的局部变量,我认为应该叫做ThreadLo ...

  7. java 内部thread_Java代码质量改进之:使用ThreadLocal维护线程内部变量

    在上文中,<Java代码质量改进之:同步对象的选择>,我们提出了一个场景:火车站有3个售票窗口,同时在售一趟列车的100个座位.我们通过锁定一个靠谱的同步对象,完成了上面的功能. 现在,让 ...

  8. 4问教你搞定java中的ThreadLocal

    摘要:ThreadLocal是除了加锁同步方式之外的一种保证规避多线程访问出现线程不安全的方法. 本文分享自华为云社区<4问搞定java中的ThreadLocal>,作者:breakDra ...

  9. TheadLocal的用法

    我们知道Spring通过各种模板类降低了开发者使用各种数据持久技术的难度.这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突.我们使用模板类访问底层数据,根据持久化技术 ...

最新文章

  1. zookeeper 分布式协调服务
  2. Linux运维工程师的十个基本技能点
  3. CSS之七个高度有效的媒体查询技巧
  4. c语言链表实现数组逆置,数组与链表等顺序表逆置
  5. Spring AOP切入点与通知XML类型
  6. 物联网常用的无线通信技术
  7. java编程算法出现在窗口_Java实现轨迹压缩算法开放窗口代码编程实例分享
  8. linux运维 对比 网络_linux - 终端下查看网络实时吞吐量
  9. kafka知识 --kafka权威指南
  10. 基于RFID定位技术的智能仓储管理系统--RFID智能仓储--新导智能
  11. 我的世界java平台缺少证书_解决https安全证书缺少的问题
  12. [深度学习工具]·FoolNLTK 中文处理工具包使用教程
  13. matlab qua2d,matlab 几个关于GPS/INS和GPS/AHRS的程序 - 下载 - 搜珍网
  14. mac 不显示 外接屏幕_教大家Mac外接显示器设置教程
  15. 用Python画圣诞树
  16. 网络爬虫(python项目)
  17. 王者荣耀背后的实时大数据平台用了什么黑科技?
  18. Apriori算法和FP-Tree算法简介
  19. websocket以及nodejs联手打造的类qq群聊天室 教程 附 原代码
  20. 交通肇事罪法院是如何量刑

热门文章

  1. optee中添加一个中断以及底层代码的相关解读
  2. SQLite 数据库注入总结
  3. 想成为别人眼里的Python大牛,就必不可少的书单
  4. 26、HTML 区块
  5. 通过cookie保存并读取用户登录信息
  6. C语言经典算法 21-30
  7. Synchronize锁对象
  8. MySQL事务的特性
  9. 一款IDEA插件神器,帮你一键转换DTO、VO、BO、PO、DO
  10. http简介看这篇就够了