一、前言

对ThreadLocal不熟悉的同学,可以先参考我的另外一篇文章浅谈ThreadLocal

在讨论内存泄漏之前,需要明白java中的四种引用,同样可以移步到java中的四种引用

什么是内存泄露?

大白话讲,就是我自己创建的对象,在一系列操作后,我访问不到该对象了,我认为它已经被回收掉了,但该对象却一直存在与内存中。


二、示例

先给出一个简单例子,用来说明引用与对象的指向关系

package com.yang.testThreadLocal; public class Main { private static ThreadLocal<Integer> tl = new ThreadLocal<>(); public static void main(String[] args) {

        tl.set(1);

        Thread t = new Thread(() -> {
            tl.set(2);
            System.out.println("子线程:" + tl.get());
        });
        t.start();

        System.out.println("主线程:" + tl.get());

    }
}

指向关系如下所示,主线程就不画了,各位别嫌弃图的配色,我真的是配不出来了。

在ThreadLocalMap中,维护一个Entry类型的数组,Entry是一个(k,v)结构,可以把ThreadLocalMap理解为一个定制化的HashMap。不过Entry的key是对ThreadLocal的一个弱引用,在执行tl=null后,1号线断开,则该ThreadLocal会在下一次GC到来的时候,被回收掉。

为什么这里的key保持着对ThreadLocal的一个弱引用呢?保持强引用行不行?

假设这里的key保持对ThreadLocal的强引用,则当我的程序用不到该ThreadLocal时,我手动执行了tl=null,此时1号线断开,而这里的5号线是实线,5号线没有断开,因此ThreadLocal对象无法被回收掉,一直存在于内存中,造成内存泄露。

看来,这里的弱引用,能够保证用不到的ThreadLocal被回收掉。

弱引用就能完全防止内存泄露了吗?

由上面的分析,弱引用能够防止释放不掉ThreadLocal引起的内存泄露。但是,却不能防止释放不掉Integer引起的内存泄露。首先,执行tl=null,则1号线断开,GC到来时,5号线断开,此时ThreadLocal被回收掉了,这个key被置为了null,可是这个key对应的value强引用着Integer对象,该Integer无法在用户代码中访问到了,但却依然存在于内存中,造成内存泄露。

既然依然存在着内存泄露,那么JDK团队是怎么解决的呢?

其实,ThreadLocal中的get()、set()方法,不是单纯地去做获取、设置的操作。在它们的方法内部,依然会遍历该Entry数组,删除所有key为null的Entry,并将相关的value置为null,从而够解决因释放不掉value而引起的内存泄露。

有这些get()、set()方法,就能完全地防止内存泄漏吗?

但我们手动将tl置为null后,就已经没法调用这些get()、set()方法了。所以,预防内存泄露的最佳实践是,在使用完ThreadLocal后,先调用tl.remove(),再调用tl=null。tl.remove()能够使得ThreadLocalMap删除该ThreadLocal所在的Entry,以及将value置为null,tl=null使得ThreadLocal对象真正地被回收掉。