ThreadLocal的内存泄露到底怎么回事

如果你的简历上写到了ThreadLocal,那么你面试的时候大概率会被面试官拷打到这个知识点,而且无非就是问你ThreadLocal是怎么实现的,ThreadLocal的内存泄漏是怎么来的?为什么Entry中对Key的引用是所引用,而value就不是弱引用呢?网上有很多说法,说加个static就可以了,说大多数时候都不会有这个问题,可是真的是这样吗,都知道加static可以解决一部分问题,可是有几个能说清楚为什么呢。
ThreadLocal底层是怎么实现的
要想搞清楚ThreadLocal那么多的相关知识,我们就要先搞清楚一个前提:
ThreadLocal是怎么实现的?
首先我们都知道,ThreadLocal提供了一套线程隔离的存储与获取对象的方法,但它到底是怎么实现的呢?
首先我们来看看这张图片,每一个Thread都有一个唯一对应的ThreadLocalMap,而这个ThreadLocalMap你可以把他看作一种特殊的HashMap,因为它也是Map结构,存储的是Entry数组,相比HashMap比较不同的是,所有的Key都是ThreadLocal(这里也就是说同一个线程可以声明多个ThreadLocal变量,所以同一线程下,ThreadLocal不是唯一的,但是ThreadLocalMap绝对是唯一的)。
我们再来看看对象之间的引用关系。与其说每个ThreadLocalMap存储了Entry,不如说ThreadLocalMap存储了一个Entry数组,这个数组存储的每个Entry实际上是指针,也就是说这些Entry实际上是引用,真正的Entry,对应的key-value还是存储在堆上的。
内存泄漏是什么?
我们先来讲讲内存泄漏是什么吧。
用C++举个例子:
C++的内存空间的创建和释放都是要我们手动管理的。
假如说你new了一个对象,用指针指向了它。
你还没有通过delete释放它,你就讲原本用于存储它内存地址的指针指向了另一个地址。
那么你就再也拿不到你原来new的那个对象了,你再也无法对这块空间进行操作了。
但是这个对象依旧会保存在内存中,你无法释放它,它永远会占用一块内存空间。
这就是我们说的内存泄露问题,简单来讲,就是有一块内存空间我们无法进行控制和利用了。
放到Java来看,如果有一个对象,你明明不需要他了,可是从你的代码层面,你无法去释放它,而垃圾回收器还以为这个对象不能被回收,于是这个对象就会一直浪费着这一块的空间。
久而久之 浪费的空间越来越多,要么面临Full GC,要么面临OOM。
ThreadLocal内存泄露?设为static就没事了?
线程的正常生命周期下会存在内存泄漏问题吗?
我们讲一下一条正常的Thread从创建到销毁的生命周期下,ThreadLocal是否有内存泄漏问题。
新建一条线程,然后我们new了一个ThreadLocal,当第一次调用ThreadLocal的set方法时,会去查询该条线程是否有对应的ThreadLocalMap了,如果没有的话,就会在set之前先创造ThreadLocalMap。接着把该ThreadLocal存入到ThreadLocalMap中,将set的value存为对应的值。
1 | 另外这里想提一嘴,ThreadLocalMap解决冲突的方式是开放定址法,而HashMap解决冲突是链式定址法。 |
我们忽略到中途的调用细节,我们来说,到最后这条线程被释放了,会发生什么。
如上两张图,由于Thread被销毁了,那么堆上的ThreadLocalMap也就失去了强引用,而失去强引用的对象在下一次GC到来时都会被回收的。
于是ThreadLocalMap就被回收了。
同理,Entry失去了ThreadLocalMap中Entry数组中对其的引用,Entry也被回收。
接下来 ThreadLocal与被存储的对应的value也被回收。
所以大家是可以看到的 只要Thread被销毁,根本就不存在ThreadLocal的内存泄漏问题。
线程池场景下的内存泄漏
刚刚说到的场景,是单挑线程被正常创建与释放的。
那线程池下是怎样的呢?要知道SpringBoot的默认容器可就是tomcat,而tomcat底层是基于线程池实现的,每一条请求都对应一条线程。
一条请求过来之后,如果说我们用的是局部变量new了一个ThreadLocal,那么会发生什么呢?
你会发现,由于Thread不会被销毁,那么ThreadLocalMap也就不会销毁,我们又没有对ThreadLocal做释放。
逻辑上讲,上一条请求的ThreadLocal的值对我们来说是没有用的。
而我们也拿不到上一条请求的地址,而ThreadLocalMap对该entry的应用依旧存在,也就是说,这个我们用不到也拿不到的entry,依旧会存在于我们的内存空间中。
Entry对key的弱引用
这里的Entry对key,也就是ThreadLocal的引用,实际上是弱引用。被弱引用的对象如果没有其他的引用,那么下一次GC必定会被回收,也就是说,实际上ThreadLocal这个对象,我们是不存在内存泄漏问题的。
真正存在内存泄露的,实际上是ThreadLocal中对应存的Value。entry对value可不是弱引用。
那么有的人就会问了,为什么不把value也设为弱引用?
这是个很好的问题,我们先来补充点前置知识吧。
强引用:我们能看到的显式的声明基本都是强应用,比如:
List
这里的list就是对堆上的arraylist有一个强引用。
弱引用需要我们继承weakReference。而被弱引用的对象,如果没有其他的强引用或软引用(有兴趣可以自行了解)。
下一次GC必然会被回收。
那么我们再来看原来的场景:
这条请求结束了,那么实际上ThreadLocal的强引用也就不存在了,只剩下了一个弱引用,ThreadLocal也就被回收了。
为什么不把value设为弱引用呢?接着看这张图,你会发现,我们没有对value设置一个显式的强引用,也就是说,我们直接将他存入了ThreadLocal中,如果value也是弱引用,而外部没有强引用,下一次GC,那么ThreadLocal中的value就会被回收。可能我们代码的业务链还没走完,value就被回收了。
如何解决ThreadLocal的内存泄漏问题?
首先来说最完美的解决方案:
调用ThreadLocal的remove方法。
先调用ThreadLocal中的remove方法,然后对ThreadLocal进行置空,这是最好的解决方法。
这样一点都不用担心内存泄露的问题,实际开发中建议将ThreadLocal的生命周期管理在Interceptor中处理,或者用AOP去处理。
将ThreadLocal封装成某个类的静态成员变量
将ThreadLocal设为静态变量,这样能保证,不会被销毁的那条线程,所对应的ThreadLocal是唯一的。
苍穹外卖里就是这么写的:
由于ThreadLocal是唯一的,当当前请求结束下一条请求进入时,调用了ThreadLocal的set方法,原来的存储对象就会被覆盖:
但是依旧有一定的缺陷,比如如果长时期没有请求,里面永远要存着那个对象,假如存的是一个超级大的对象呢?及其占空间,对吧。所以最好还是remove一下。
- 标题: ThreadLocal的内存泄露到底怎么回事
- 作者: Sal
- 创建于 : 2025-07-23 21:04:34
- 更新于 : 2025-07-25 22:19:15
- 链接: https://redefine.ohevan.com/posts/58566/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。