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

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的内存泄漏

java 复制代码
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是弱引用

java 复制代码
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问题。

相关推荐
Scott9999HH7 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔7 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社8 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海8 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖9 小时前
JDK 26 新特性详解
java·开发语言
马优晨10 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大12 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai12 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说12 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json