ThreadLocal内存泄漏 强引用vs弱引用

发布于:2025-07-08 ⋅ 阅读:(18) ⋅ 点赞:(0)

ThreadLocal内存泄漏 强引用vs弱引用

ThreadLocal常常被我们用来做共享变量的线程间隔离访问,它的内存泄漏问题是我们老生常谈的问题了,今天来记录一下对它的深入学习。

ThreadLocalMap

每个线程都有一个ThreadLocalMap类型的变量,我们可以把它看作一个Map。像所有Map结构一样,其中有一个静态内部类Entry来记录着每一组映射关系。我们在线程中使用new ThreadLocal()去创建一个Threadlocal对象,然后调用这个对象的set方法去设置值的时候,其实就对应着在这个线程的ThreadLocalMap中增加一个Entry,其中key是我们新建的ThreadLocal对象,value就是我们设置的值。

不同与常规Map结构的是,ThreadLocalMap中对key和value的引用不全是强引用,对key的引用是弱引用

强引用vs弱引用

Java中一个有四种引用类型,强引用弱引用、软引用、虚引用。这里讨论一下强引用和弱引用,软引用和虚引用并不在讨论范围之内。

强引用

强引用就是我们平常见的最多的一种引用,使用new关键字创建出来的引用,强引用的对象只要引用还在就不会被GC回收。

弱引用

弱引用(WeakReference)是只要GC发生时,这个引用就会被回收的一种引用,用来释放内存。

内存泄漏

在学习ThreadLocal的时候,教程中总是会有一句使用完后一定要进行remove操作,这样可以避免内存泄露问题,但是具体是为什么呢?为什么不调用remove方法时就会内存泄露呢?key会内存泄露嘛?

key和value的内存泄漏

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

先看上面的代码,我们把tl引用的ThreadLocal对象的地址看作是0x123,user引用的UserDTO对象的地址看作是0x456。

每当一个线程使用saveUser方法时,都会在自己线程的ThreadLocalMap中新增一个Entry(key,value),key引用0x123,value引用着0x456。其中Entry对key的引用是弱引用,对value的引用是强引用。但是由于tl是UserHolder类的静态变量,只要这个类不被卸载,那么tl一直持有着对0x123的强引用。所以这里,user由于是被Entry持有着强引用而不会被GC,tl是本身也是强引用也不会被GC,当我们最后不使用remove方法将Entry对象置为null的话,那么这个Entry对象就会一直在内存中存在着,就会造成内存泄露问题。

为什么key是弱引用

public class UserHolder {
    public static void doSomething(UserDTO user){
         private ThreadLocal<UserDTO> tl = new ThreadLocal<>();
        tl.set(user); 
    }
}

考虑上面的情景,一个线程来使用某个方法,只在这个方法中用到了ThreadLocal变量来进行线程隔离,但是使用完了这个方法之后出了这个方法,那么tl对0x123的强引用就没了,但是我们忘记了对使用remove方法,此时弱引用的好处就出来了,下一次gc发生时,由于ThreadLocalMap对0x123的引用是弱引用就会被回收,那么这个线程中的ThreadLocalMap就会有一个Entry(null,value)的结构,下次在ThreadLocalMap内部的set,get和扩容时都会清理掉泄漏的Entry

为什么value不设置成弱引用

首先gc的时机我们是不好把握的,如果把value设置成弱引用的话,只要value对象的强引用没了,那么value就随时会丢失。我们通过key就找不到这个value了,这就会产生问题。而key设置成弱引用我们不会有这个担心的原因是,ThreadLocalMap的主要目的就是通过key来找value,我们默认key在能get到value,如果key不在就证明这个Entry失效了。

为什么要避免内存泄漏

一个线程的生命周期结束后,那么这个线程栈也会被销毁,即使这个线程中发生了内存泄漏我们应该也不必太过担心啊?那么为什么开发中我们那么关心这个内存泄漏问题呢?

原因是因为开发中我们基本都会使用线程池来管理线程,所以用完了一个线程,这个线程往往不会被销毁,而是放回到线程池中,如果我们不管这个线程的内存泄漏问题的话,每次使用之后这个线程栈可用内存都会减少,最终可能会导致后面的请求使用的时候出现OOM问题。


网站公告

今日签到

点亮在社区的每一天
去签到