写点什么

ThreadLocal 真的会造成内存泄漏吗?

  • 2024-01-15
    福建
  • 本文字数:2501 字

    阅读完需:约 8 分钟

ThreadLoca 在并发场景中,应用非常多。那 ThreadLocal 是不是真的会造成内存泄漏?今天给大家做一个分享,个人见解,仅供参考。


1、ThreadLocal 的基本原理


简单介绍一下 ThreadLocal,在多线程并发访问同一个共享变量的情况下,如果不做同步控制的话,就可能会导致数据不一致的问题,所以,我们需要使用 synchronized 加锁来解决。而 ThreadLocal 换了一个思路来处理多线程的情况,



ThreadLocal 本身并不存储数据,它使用了线程中的 threadLocals 属性,threadLocals 的类型就是在 ThreadLocal 中的定义的 ThreadLocalMap 对象,当调用 ThreadLocal 的 set(T value)方法时,ThreadLocal 将自身的引用也就是 this 作为 Key,然后,把用户传入的值作为 Value 存储到线程的 ThreadLocalMap 中,这就相当于每个线程的读写操作都是基于线程自身的一个私有副本,线程之间的数据是相互隔离的,互不影响。这样一来基于 ThreadLocal 的操作也就不存在线程安全问题了。它相当于采用了用空间来换时间的思路,从而提高程序的执行效率。


2、四种对象引用


在 ThreadLocalMap 内部,维护了一个 Entry 数组 table 的属性,用来存储键值对的映射关系,来看这样一段代码片段:


static class ThreadLocalMap {     ...    private Entry[] table;    static class Entry implements WeakReference<ThreadLocal<?>> {        Object value;        Entry(ThreadLocal<?> k, Object v) {            super(k);            value = v;        }    }    ...}
复制代码


Entry 将 ThreadLocal 作为 Key,值作为 Value 保存,它继承自 WeakReference,注意构造函数里的第一行代码 super(k),这意味着 ThreadLocal 对象是一个「弱引用」。有的小伙伴可能对「弱引用」不太熟悉,这里再介绍一下 Java 的四种引用关系。在 JDK1.2 之后,Java 对引用的概念做了一些扩充,将引用分为“强”、“软”、“弱”、“虚”四种,由强到弱依次为:



强引用:指代码中普遍存在的赋值行为,如:Object o = new Object(),只要强引用关系还在,对象就永远不会被回收。


软引用:还有用处,但不是必须存活的对象,JVM 会在内存溢出前对其进行回收,例如:缓存。


弱引用:非必须存活的对象,引用关系比软引用还弱,不管内存是否够用,下次 GC 一定回收。


虚引用:也称“幽灵引用”、“幻影引用”,最弱的引用关系,完全不影响对象的回收,等同于没有引用,虚引用的唯一的目的是对象被回收时会收到一个系统通知。


这个描述还是比较官方的,简单总结一下,大家应该都追过剧,强引用就好比是男主角,怎么都死不了。软引用就像女主角,虽有一段经历,还是没走到最后。弱引用就是男二号,注定用来牺牲的。虚引用就是路人甲了。


3、造成内存泄漏的原因


内存泄漏和 ThreadLocalMap 中定义的 Entry 类有非常大的关系。


3.1 内存泄漏相关概念


Memory overflow:内存溢出,没有足够的内存提供申请者使用。Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。I 内存泄漏的堆积终将导致内存溢出。


3.2 如果 key 使用强引用


假设 ThreadLocalMap 中的 key 使用了强引用,那么会出现内存泄漏吗?此时 ThreadLocal 的内存图(实线表示强引用)如下:



  1. 假设在业务代码中使用完 ThreadLocal, ThreadLocal ref 被回收了

  2. 但是因为 threadLocalMap 的 Entry 强引用了 threadLocal, 造成 ThreadLocal 无法被回收

  3. 在没有手动删除 Entry 以及 CurrentThread 依然运行的前提下, 始终有强引用链 threadRef → currentThread → entry, Entry 就不会被回收( Entry 中包括了 ThreadLocal 实例和 value), 导致 Entry 内存泄漏也就是说: ThreadLocalMap 中的 key 使用了强引用, 是无法完全避免内存泄漏的


3.3 如果 key 使用弱引用


假设 ThreadLocalMap 中的 key 使用了弱引用, 那么会出现内存泄漏吗?



  1. 假设在业务代码中使用完 ThreadLocal, ThreadLocal ref 被回收了

  2. 由于 threadLocalMap 只持有 ThreadLocal 的弱引用, 没有任何强引用指向 threadlocal 实例, 所以 threadlocal 就可以顺利被 gc 回收, 此时 Entry 中的 key = null

  3. 在没有手动删除 Entry 以及 CurrentThread 依然运行的前提下, 也存在有强引用链 threadRef → currentThread → value, value 就不会被回收, 而这块 value 永远不会被访问到了, 导致 value 内存泄漏也就是说: ThreadLocalMap 中的 key 使用了弱引用, 也有可能内存泄漏。


3.4 内存泄漏的真实原因



出现内存泄漏的真实原因出改以上两种情况比较以上两种情况,我们就会发现:内存泄漏的发生跟 ThreadLocalIMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?细心的同学会发现,在以上两种内存泄漏的情况中.都有两个前提:


  1. 没有手动侧除这个 Entry


  1. CurrentThread 依然运行


第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法翻除对应的 Entry ,就能避免内存泄漏。第二点稍微复杂一点,由于 ThreodLocalMap 是 Threod 的一个属性,被当前线程所引甲丁所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前 Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。


综上, ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏.


4、如何避免内存泄漏?


不要听到「内存泄漏」就不敢使用 ThreadLocal,只要规范化使用是不会有问题的。我给大家支几个招:


1、每次使用完 ThreadLocal 都记得调用 remove()方法清除数据。


2、将 ThreadLocal 变量尽可能地定义成 static final,避免频繁创建 ThreadLocal 实例。


这样也就保证程序一直存在 ThreadLocal 的强引用,也能保证任何时候都能通过 ThreadLocal 的弱引用访问到 Entry 的 Value 值,进而清除掉。


当然,就是使用不规范,ThreadLocal 内部也做了一些优化,比如:


1、调用 set()方法时,ThreadLocal 会进行采样清理、全量清理,扩容时还会继续检查。


2、调用 get()方法时,如果没有直接命中或者向后环形查找时也会进行清理。


3、调用 remove()时,除了清理当前 Entry,还会向后继续清理。


文章转载自:咬到舌头的小蛇

原文链接:https://www.cnblogs.com/zhaotianen/p/17962653

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
ThreadLocal真的会造成内存泄漏吗?_内存泄露_不在线第一只蜗牛_InfoQ写作社区