ThreadLocal是否存在内存泄漏问题,如何防止内存泄漏

发布时间 2023-05-28 22:45:10作者: BlogZero

ThreadLocal还是不能百分百地让程序员避免内存泄露,如果程序员不谨慎就很可能导致

内存泄露?那么今天我们就来聊聊什么样的情况ThreadLocal不会出现内存泄露?什么样的情况会出现内存泄露?我们如何防止内存泄露的情况发生呢?

我们这节就会为同学们一一详细解答,那我们先来简单回忆一下ThreadLocal的底层实现,请看下图图1

图1

 

通过图1我们能清晰的看到JDK的线程本地化ThreadLocal是存储在每个线程Thread内部的ThreadLocalMap里的,且ThreadLocalMap包含了一个Entry数组,

每个Entry存储着一个ThreadLocal和对应的value值,也就是说Thread可以存储多个ThreadLocal对象,既然我们对ThreadLocal的底层实现有所了解,

那么问题来了,如何使用ThreadLocal不会出现内存泄露情况呢?请看下图图2

图2

 

从图图2中,我们能看出,假如我们使用一个线程thread来执行任务,当thread线程执行完任务退出之后,该线程里所持有的ThreadLocalMap

的对象也就

没有了强引用,那么由于ThreadLocalMap没有强引用,所以就可以被JDK垃圾回收器回收了,那么ThreadLocalMap里面所

包含ThreadLocal也就回收掉了。详细的整个流程,请看下图图3

图3

 

到了这里想必同学们应该知道之前如何使用ThreadLocal不会出现内存泄露情况问题的答案了吧,对,它就是创建Thread或者Thread子类来执行任务处理,

随着对应的线程Thread生命周期结束,那么线程Thread所持有的ThreadLocal也会被垃圾回收,不会出现内存泄露情况发生。

 

但是每执行一个任务都要创建一个Thread来处理,对于机器来说开销还是不小的,我们之前文章中讲到,

可以用到线程池的技术来解决频繁的创建和销毁Thread。那么在使用线程池的场景下使用ThreadLocal是否会有内存泄露的情况发生呢?那我们先来看一下图图4

图4

 

在之前的学习中,我们知道线程池里的核心线程Thread执行完任务之后,是不会退出的,可以循环使用的,那就说明线程池里每个核心线程Thread

对应的ThreadLocalMap一直是强引用关系,所以线程Thread对应的ThreadLocal是不会自动回收的,那同学们可能会说,

之前章节不是说了ThreadLocal是WeakReference弱引用,JDK触发垃圾回收的时候可以自动回收吗?同学们说的都没有问题,我们先来看一下图图5

图5

 

在上述图5中,我们用新的一张图来表示Thread和ThreadLocal对应关系,其中ThreadLocalMap中的Entry中的key是属于WeakReference弱引用,

随着JDK的垃圾回收ThreadLocal可以自动被回收,那么我们看一下触发JDK垃圾回收之后的示意图,请看下图图6

在JDK触发垃圾回收之后,对应的ThreadLocal确实可以被垃圾回收掉,变为null值,但是同学们,被自动回收的ThreadLocal所对应value值

是不能被自动回收的,请看下图图7

图7

 

在上述图7,我们能清晰看到ThreadLocal的key是可以被自动回收变成为null,但是对应的value还是被Entry引用着呢,所以value是不能被垃圾回收器自动回收的,到了这里,想必同学们应该知道了,如果在线程池场景中使用ThreadLocal是有内存泄露情况的可能性,原因就是线程池的核心线程Thread是循环利用的,

每个线程Thread所对应的ThreadLoalMap被强引用着,所以每个线程Thread的ThreadLoalMap不能被回收,但是ThreadLoalMap里

含有多个ThreadLocal-value的Entry,虽然ThreadLocal-key是弱引用可以被垃圾回收器自动回收,但是ThreadLocal对应的value是不能被回收的,

所以说有内存泄露的情况可能性。那同学们可能会问,这种情况,不能避免吗?那我们先看一下JDK是否解决方案呢?请看下图图8

图8

 

上述图8,同学们应该还有印象吧,它就是我们之前讲解的ThreadLocal的get方法,先是在红线1处就是获取ThreadLocal对应的Entry,然后再从Entry

获取对应value,那么在红线2处,我们能看到这个if条件,同学们,如果Entry所对应的ThreadLocal被自动回收变成null啦,那这个if判断条件是不成立的,

就会走到getEntryAfterMiss这个方法里对吧,那么就来看看getEntryAfterMiss这个方法的实现,请看下图图9

图9

 

从图9我们能够看到getEntryAfterMiss的逻辑,我们传进来的Entry e其实所对应的key,也就是ThreadLocal是为null的,所以一定会走到上图红线处这个条件里,会走到expungeStaleEntry这个方法里,我们再来看看expungeStaleEntry这个方法的逻辑,请看下图图10

图10

 

从上图图10红线1处,我们能清晰的看到,会把ThreadLocal为null所对应的value设置为null,同时把对应的Entry也设置为null,同时在红线2处,

会遍历所有的为ThreadLocal为null的value和对应的Entry都设置为null,这样就去除了强引用,有助于被垃圾回收,我们再来看一下所对于的示意图图11

图11

 

到了这里,同学们可能会说,JDK的expungeStaleEntry的方法不是会把ThreadLocal为null所对应value和Entry对象设置为null嘛,这样就可以被垃圾回收啦,

那在线程池的使用场景下就不会出现内存泄露的情况了啊?同学们,只有在调用ThreadLocal的get、set、remove方法的时候才会触发expungeStaleEntry方法

的执行,才会把被自动垃圾回收的ThreadLocal为null所对应的value和Entry才会设置为null。换句话说,正常的情况是不会出现内存泄露的,但是如果我们

没有调用ThreadLocal对应的set、get、remove方法就不会把对应的value和Entry设置为null,这样就可能会出现内存泄露情况。对吧,那如何避免内存泄露

的情况呢?那就是我们在不使用的时候就调用一下ThreadLoca的remove方法,来加快垃圾回收,避免内存泄露。

 

最后简单总结一下,由于ThreadLocalMap包含了ThreadLocal,且线程Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期

是相同的,所以在线程池场景中使用ThreadLocal的时候,我们还是要养成好习惯,ThreadLocal不在使用的时候调用remove方法,避免内存泄漏情况发生。

 

转:https://zhuanlan.zhihu.com/p/554781932