【面试进阶之路】这次彻底说明白 ThreadLocal 内存泄漏问题

欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!

在我后台回复 「资料」 可领取编程高频电子书
在我后台回复「面试」可领取硬核面试笔记

文章导读地址:点击查看文章导读!

感谢你的关注!

在这里插入图片描述

这次彻底说明白 ThreadLocal 内存泄漏问题

ThreadLocal 用于存储线程本地的变量,如果创建了一个 ThtreadLocal 变量,在多线程访问这个变量的时候,每个线程都会在自己线程的本地内存中创建一份变量的副本,从而起到线程隔离的作用

Thread、ThreadLocal、ThreadLocalMap 之间的关系:
在这里插入图片描述

每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程所有的ThreadLocal对象及其对应的值

ThreadLocalMap由一个个的Entry<key,value>对象构成,Entry继承自weakReference<ThreadLocal<?>>,一个EntryThreadLocal对象和Object构成

  • Entry 的 key 是ThreadLocal对象,并且是一个弱引用。当指向key的强引用消失后,该key就会被垃圾收集器回收
  • Entry 的 value 是对应的变量值,Object 对象

当执行set方法时,ThreadLocal首先会获取当前线程 Thread 对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,获取对应的 value。

由于每一条线程均含有各自私有的 ThreadLocalMap 对象,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也就无需使用同步机制来保证多条线程访问容器的互斥性

ThreadLocal 使用场景:

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传送,打破层次间的约束。

即如果一个User对象需要从Controller层传到Service层再传到Dao层,那么把User放在ThreadLocal中,每次使用ThreadLocal来进行获取即可

2、线程间数据隔离

3、进行事务操作,用于存储线程事务信息

4、数据库连接,Session会话管理

ThreadLocal 的内存泄漏问题:

在这里插入图片描述

这里假设将 ThreadLocal 定义为方法中的局部变量,那么当线程进入该方法的时候,就会将 ThreadLocal 的引用给加载到线程的栈 Stack 中

如上图所示,在线程栈 Stack 中,有两个变量,ThreadLocalRef 和 CurrentThreadRef,分别指向了声明的局部变量 ThreadLocal ,以及当前执行的线程

而 ThreadLocalMap 中的 key 是弱引用,当线程执行完该方法之后,Stack 线程栈中的 ThreadLocalRef 变量就会被弹出栈,因此 ThreadLocal 变量的强引用消失了,那么 ThreadLocal 变量只有 Entry 中的 key 对他引用,并且还是弱引用,因此这个 ThreadLocal 变量会被回收掉,导致 Entry 中的 key 为 null,而 value 还指向了对 Object 的强引用,因此 value 还一直存在 ThreadLocalMap 变量中,由于 ThreadLocal 被回收了,无法通过 key 去访问到这个 value,导致这个 value 一直无法被回收,ThreadLocalMap 变量的生命周期是和当前线程的生命周期一样长的,只有在当前线程运行结束之后才会清除掉 value,因此会导致这个 value 一直停留在内存中,导致内存泄漏

当然 JDK 的开发者想到了这个问题,在使用 set get remove 的时候,会对 key 为 null 的 value 进行清理,使得程序的稳定性提升。

当然,我们要保持良好的编程习惯,在线程对于 ThreadLocal 变量使用的代码块中,在代码块的末尾调用 remove 将 value 的空间释放,防止内存泄露。

ThearLocal 内存泄漏的根源是:

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

ThreadLocal 正确的使用方法:

  • 每次使用完 ThreadLocal 都调用它的 remove() 方法清除数据
  • 将 ThreadLocal 变量定义成 private static final,这样就一直存在 ThreadLocal 的强引用,也能保证任何时候都能通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,进而清除掉

下面给出 ThreadLocal 的用法:

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                int value = counter.get(); // 获取当前线程的副本值
                counter.set(value + 1); // 修改副本值
                System.out.println("Thread " + Thread.currentThread().getName() + " value: " + counter.get());
            } finally {
                // 手动移除
                counter.remove(); // 在线程结束时移除变量
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                int value = counter.get();
                counter.set(value + 1);
                System.out.println("Thread " + Thread.currentThread().getName() + " value: " + counter.get());
            } finally {
                // 手动移除
                counter.remove();
            }
        });

        t1.start();
        t2.start();
    }
}

那么 ThreadLocal 为什么要将 key 设计为弱引用呢?

这里还要看一下具体是如何使用 ThreadLocal 了

  • 如果定义 ThreadLocal 为局部变量,那么这个 ThreadLocal 对象就会放在堆中,如果不手动 remove() 的话,当线程执行完当前方法退出时,这个局部变量对 ThreadLocal 的强引用就消失了,只剩下 Thread.ThreadLocalMap 中的 key 对 ThreadLocal 的弱引用,因此会将 ThreadLocal 给回收掉,而 value 还存在强引用,而我们没有了 TheadLocal 的引用导致访问不到 value,导致 value 无法回收,因此 JDK 设计者在 ThreadLocal 还添加了清除 ThreadLocalMap 中 key 为 null 的 value,避免内存泄漏,这是在设计时为了避免内存泄漏而采取的措施,而我们使用的时候要保持良好的编程规范,也要手动去 remove,避免内存泄露的发生
  • 如果定义 ThreadLocal 为 private static final,那么这个 ThreadLocal 就会在常量池中存储,而不是存储在堆中,这时候要考虑的问题是当前线程在使用完 ThreadLocal 之后要主动 remove 避免出现脏数据(而不是内存泄漏问题,因为我们可以随时通过该 ThreadLocal 去访问到 ThreadLocalMap 中的 value 值,并随时进行回收,因此不会存在内存泄漏),因为在多线程的环境中,如果上一个线程使用完 ThreadLocal 之后并没有 remove,下一个线程来使用时可能会拿到上个线程的数据,产生了脏数据

总结一下:

那么这里总结一下,将 ThreadLocal 定义为局部变量,会导致方法执行完之后 ThreadLocal 被回收,而 value 没有被回收,导致无法通过 key 访问到这个 value,导致内存泄漏

如果规范使用,将 ThreadLocal 定义为 private static final,那么这个 ThreadLocal 不会被回收,可以随时通过这个 ThreadLocal 去访问到 value,随时可以手动回收,因此不会内存泄漏,但是会导致脏数据

所以在 ThreadLocal 的内存泄漏问题主要是针对将 ThreadLocal 定义为局部变量的时候,如果不手动 remove 可能会导致 ThreadLocalMap 中的 Entry 对象无法回收,一直占用内存导致内存泄漏,直到当前 Thread 结束之后才会被回收

这里再说一下 ThreadLocal 的使用规范就是:将 ThreadLocal 变量定义为 private static final,并且在使用完,记得通过 try finall 来 remove 掉,避免出现脏数据

(以上均为个人见解,如有不足,欢迎指正)

  • 21
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ThreadLocal 内存泄露问题是指当使用ThreadLocal类时,如果没有正确地进行清理和处理,就有可能导致内存泄露的情况发生。这是因为ThreadLocal对象的生命周期与线程的生命周期相对独立,当线程结束时,ThreadLocal对象没有被垃圾回收,且其中存储的数据也无法被访问,从而导致内存泄露。 具体来ThreadLocal类通过操作ThreadLocalMap来存储每个线程的数据。当一个线程结束时,如果没有正确地清理ThreadLocal对象,那么ThreadLocalMap中与该线程相关的条目将无法被删除。这意味着,即使这些条目对应的线程不再活跃,它们却仍然占据着内存空间。 一种常见的导致ThreadLocal内存泄露的情况是在使用完ThreadLocal对象后未调用其remove方法进行清理操作。如果在一个长时间运行的线程中重复使用ThreadLocal对象,而不进行清理操作,就会导致ThreadLocalMap中的条目越来越多,从而造成内存泄露。 另外,当ThreadLocal对象被作为静态变量使用时,也容易出现内存泄露问题。因为静态变量的生命周期很长,如果静态ThreadLocal对象没有被妥善处理,那么其中的数据也将无法被释放。 为了避免ThreadLocal内存泄露,应该养成良好的编程习惯,确保在使用完ThreadLocal对象后,及时调用其remove方法进行清理。另外,如果ThreadLocal对象被用作静态变量,也应该在不再使用时手动将其置为null,以便让垃圾回收器能够回收相关的内存空间。 参考资料: :可以发现问题ThreadLocal已经被清理掉了,代表现在已经没有方式去访问当前ThreadLocal存到Map里的value数据了 。 :ThreadLocal就相当于一个访问工具类,通过操作ThreadLocal对象的方法 来操作存储在当前线程内部的ThreadLocalMap里的值 。 :一篇文章我们来分析一个JavaThreadLocal内存泄露的案例。分析问题的过程比结果更重要,理论结合实际才能彻底分析出内存泄漏的原因。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

11来了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值