likes
comments
collection
share

ThreadLocal的双刃剑:内存泄漏的挑战与对策ThreadLocal相当于私人储物空间,但是你想想,你的储物空间上

作者站长头像
站长
· 阅读数 40

引言

前文我说过ThreadLocal相当于私人储物空间,但是你想想,你的储物空间上面还背了个山,你还怎么行动,直接就像大圣压被在五指山下了,啥也干不了。内存泄漏就是这样子的,只要你不注意,你就挂了。

一、ThreadLocal的工作原理

1.1 ThreadLocal的存储机制

ThreadLocal提供了线程局部变量,使得每个使用该变量的线程都有独立的变量副本,避免了多线程间的数据共享问题。在Java中,ThreadLocal的存储机制主要依赖于Thread类内部的ThreadLocalMap(成员变量)。

每个线程(Thread对象)内部都有一个ThreadLocalMap类型的成员变量,它是一个自定义的哈希映射。这个映射表将ThreadLocal对象作为键(Key),将线程局部变量的值作为值(Value)。当线程访问通过ThreadLocal定义的局部变量时,它会在自己的ThreadLocalMap中查找对应的值。

ThreadLocal的双刃剑:内存泄漏的挑战与对策ThreadLocal相当于私人储物空间,但是你想想,你的储物空间上

1.2 ThreadLocalMap的结构和作用

ThreadLocalMapThreadLocal类的一个内部静态类,是一个定制的哈希映射,仅适用于维护线程本地值。不会将任何操作导出到 ThreadLocal 类之外。该类是包私有的,允许在类 Thread 中声明字段。为了帮助处理非常大且长期存在的使用情况,哈希表条目使用弱引用作为键。但是,由于不使用引用队列,因此仅当表开始空间不足时才保证删除陈旧条目。

ThreadLocal的双刃剑:内存泄漏的挑战与对策ThreadLocal相当于私人储物空间,但是你想想,你的储物空间上

1.2.1 结构特点
  • 键(Key) :ThreadLocalMap的键是ThreadLocal对象的弱引用,这有助于防止内存泄漏,因为当没有强引用指向ThreadLocal对象时,它将被垃圾回收。

  • 值(Value) :值是线程局部变量的实际数据,每个线程通过ThreadLocal存储的变量值都存储在各自线程的ThreadLocalMap中。

注意ThreadLocalMap没有采用软引用或弱引用来引用键,这与WeakHashMap不同,因为ThreadLocalMap的生命周期与线程相同,不需要自动回收。

1.2.2 作用
  • 提供线程安全的局部变量存储,每个线程可以访问自己的局部变量,而不会影响到其他线程。

  • 通过将变量存储在每个线程的ThreadLocalMap中,避免了同步操作,提高了访问效率。

  • 由于每个线程都有自己的ThreadLocalMap,因此ThreadLocal提供了一种避免使用同步的线程局部存储方式。

  • 线程局部变量的生命周期与拥有它的线程的生命周期相同。当线程结束时,存储在ThreadLocalMap中的线程局部变量也会随之被垃圾回收器回收,除非这些变量被外部强引用所引用。

二、内存泄漏的原因

生命周期:当通过ThreadLocalset方法设置变量值时,会在当前线程的ThreadLocalMap中创建一个键值对。

当线程执行完毕,如果没有外部强引用指向这些局部变量,那么随着线程的结束,这些局部变量将会被垃圾回收。

如果ThreadLocal对象被垃圾回收,但是线程仍然存活,那么这个ThreadLocal对象所关联的线程局部变量可能会成为内存泄漏的源头,因为ThreadLocalMap中的Entry对象仍然持有对这个ThreadLocal对象的强引用。

通过上面的生命周期,大家就可以看出内存泄漏的核心原因就是没有及时有效的去清理;为了避免内存泄漏,通常建议在不再需要ThreadLocal变量时,通过调用ThreadLocalremove方法来手动清除当前线程的ThreadLocalMap中对应的键值对。这样可以确保及时回收不再使用的局部变量,防止内存泄漏。

2.1 内存泄漏场景

常见的内存泄漏场景有以下几点:

ThreadLocal的双刃剑:内存泄漏的挑战与对策ThreadLocal相当于私人储物空间,但是你想想,你的储物空间上

三、内存泄漏的影响与检测方法

对于内存泄漏和检测,是一个非常大的命题,这里不多做描述,下面的脑图是一些简单的整理,可参考。

ThreadLocal的双刃剑:内存泄漏的挑战与对策ThreadLocal相当于私人储物空间,但是你想想,你的储物空间上

四、预防和解决ThreadLocal内存泄漏的策略

4.1 正确使用ThreadLocal的实践

  1. 定义为静态字段:通常将ThreadLocal变量定义为静态字段,以确保其在整个应用程序的生命周期内有效。

  2. 及时清理:在不再需要ThreadLocal变量时,应该调用remove()方法来清除当前线程的ThreadLocalMap中的对应条目。

4.2 使用try-finally块确保清理

在代码中使用try-finally块来确保无论操作是否成功,都会执行remove()方法清理资源。例如:

public static void main(String[] args) {
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    try {
        threadLocal.set("你猜我想干啥");
        // 业务代码
        String temp = threadLocal.get();
        System.out.println(temp);
        // 业务代码
    } finally {
        threadLocal.remove();
    }
}

当然,实际应用场景要根据情况来,上面的只是案例。

4.3 干货:使用WeakReference来避免强引用

ThreadLocalMap的键(Key)是对ThreadLocal对象的弱引用,这意味着当没有强引用指向ThreadLocal对象时,它将被垃圾回收器回收。但是,ThreadLocalMap中的值(Value)是强引用,如果ThreadLocal对象被回收,而线程(如线程池中的线程)仍然存活,那么ThreadLocalMap中的值将无法访问,但不会被垃圾回收器回收,从而导致内存泄漏。

为了避免这种情况,可以自定义ThreadLocal子类,在initialValue方法中返回一个WeakReference对象:

import java.lang.ref.WeakReference;

public class CustomThreadLocal extends ThreadLocal<WeakReference<String>> {

    /**
     * 自定义初始值
     *
     * @return 初始值(注意这里的返回类型由上面的泛型决定)
     */
    @Override
    protected WeakReference<String> initialValue() {
        return new WeakReference<String>("PENDING");
    }

}

这样,即使ThreadLocal对象被垃圾回收,ThreadLocalMap中的值也会因为包装在WeakReference中而被允许回收。

五、总结

内存泄漏是每个程序员都会面临的问题,我们要有敬畏之心,但是不能畏之如虎;要充分的认识问题,解析问题,方能解决问题。

希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。

同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。

感谢您的支持和理解!

转载自:https://juejin.cn/post/7419984211144425512
评论
请登录