Java ThreadLocal 与 OOM

ThreadLocal 实例通常都是 static 类型,用于关联线程和线程上下文。

ThreadLocal 提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

总结:

  • 线程并发: 在多线程并发的场景下
  • 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
  • 线程隔离: 每个线程的变量都是独立的,不会互相影响

 

一、ThreadLocal 的内部结构

JDK 早期 ThreadLocal 设计:

  • 每个 ThreadLocal 都创建一个 Map
  • 线程作为 Map 的 key,要存储的局部变量作为 Map 的 value

这样就能达到各个线程的局部变量隔离的效果。

在 JDK8 中 ThreadLocal 的设计:

  • 每个 Thread 线程内部都有一个 Map (ThreadLocalMap)
  • Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value)
  • Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。

对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

这样设计的好处有如下两个优势:

  • 每个 Map 存储的 Entry 数量会变少。因为之前的存储数量由 Thread 的数量决定,现在是由 ThreadLocal 的数量决定。在实际运用当中,往往 ThreadLocal 的数量要少于 Thread 的数量。
  • 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,减少内存的使用。

 

二、OOM

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        private Entry[] table;
        static class Entry extends WeakReference<ThreadLocal<?>> { // 弱引用

Java 中的引用有 4 种类型: 强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:

  • 强引用(StrongReference):最常见的普通对象引用,只要还有强引用指向对象,就表明对象还“活着”,垃圾回收器就不会回收这种对象。
  • 弱引用(WeakReference):垃圾回收器一旦发现了只有弱引用的对象,不管当前内存是否充足,都会回收。

如果 key 使用强引用

  1. 假设在业务代码中使用完 ThreadLocal ,ThreadLocal Ref 被回收了。
  2. 但是因为 ThreadLocalMap 的 Entry 强引用了 ThreadLocal,造成 ThreadLocal 无法被回收。
  3. 在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,始终有强引用链 threadRef -> currentThread -> threadLocalMap -> entry,Entry 就不会被回收(Entry 中包括了 ThreadLocal 实例和 value),导致 Entry 内存泄漏。

也就是说,ThreadLocalMap 中的 key 使用了强引用,是无法完全避免内存泄漏的。

如果 key 使用弱引用

  1. 同样假设在业务代码中使用完 ThreadLocal,ThreadLocal Ref 被回收了。
  2. 由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 ThreadLocal 实例,所以 ThreadLocal 就可以顺利被 gc 回收,此时 Entry 中的 key=null。
  3. 但是在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,也存在有强引用链 threadRef -> currentThread -> threadLocalMap -> entry -> value,value 不会被回收,而这块 value 永远不会被访问到了,导致 value 内存泄漏。

也就是说,ThreadLocalMap 中的 key 使用了弱引用,也有可能内存泄漏。

出现内存泄漏的真实原因

比较以上两种情况,可以发现内存泄漏的发生跟 ThreadLocalMap 中的 key 是否使用弱引用是没有关系的。在以上两种内存泄漏的情况中,都有两个前提:

  1. 没有手动删除这个 Entry
  2. CurrentThread 依然运行

第一点,只要在使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry,就能避免内存泄漏。

第二点,由于 ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 之后,如果当前 Thread 也随之执行结束,ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。

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

为什么使用弱引用

无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

要避免内存泄漏有两种方式:

  • 使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry
  • 使用完 ThreadLocal,当前 Thread 也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

也就是说,只要记得在使用完 ThreadLocal 及时的调用 remove,无论 key 是强引用还是弱引用都不会有问题。那么为什么 key 要用弱引用呢?

事实上,在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null)进行判断,如果为 null 的话,会把 value 也设置为 null 的。

这就意味着使用完 ThreadLocal,CurrentThread 依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 中的任一方法的时候会被清除,从而避免内存泄漏。

Hash 冲突

解决办法和 HashMap 不一样,ThreadLocalMap 使用的是线性探测法(开放定址法),HashMap 使用的是拉链法(链地址法)。

该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

举个例子:假设当前 table 长度为 16,也就是说如果计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,这个时候如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入。可以把 Entry[] table 看成一个环形数组。

 


https://www.bilibili.com/video/BV1N741127FH

https://blog.csdn.net/f641385712/article/details/104583169

https://blog.csdn.net/f641385712/article/details/104573489

posted @ 2021-03-06 02:56  江湖小小白  阅读(312)  评论(0编辑  收藏  举报