写ThreadLocal原理的文章太多了,笔者这里不想再分析源码,也不想剖析其实现原理,其实也并不难,就直接说下ThreadLocal的原理吧。
1、ThreadLocal原理简介
假设定义了两个ThreadLocal变量,多个线程共享使用,那么这两个ThreadLocal变量的内存数据模型是什么样的呢?
图1 ThreadLocal内存数据模型
就是每个Thread里都有一个成员变量ThreadLocalMap,ThreadLocalMap中存放的是很多个key为通过ThreadLocal哈希值计算出来的数组下标为ThreadLocal的键值对,只要你看懂了这个图,你就搞懂了ThreadLocal的原理了。
但无论是面试,还是日常开发中,许多伙伴都对ThreadLocal是否有内存泄漏都不能百分比确认。
2、ThreadLocal是否存在内存泄漏?
假如我们使用一个线程thread来执行任务,当thread线程执行完任务退出之后,该线程里所持有的ThreadLocalMap的对象也就没有了强引用,同时Entry里的两个ThreadLocal也没有了强引用,由于ThreadLocalMap和Entry里的ThreadLocal都没有了强引用,所以就可以被JDK垃圾回收器回收了,就不存在内存泄漏了。
图2 ThreadLocal垃圾回收
所以大家应该知道如何使用ThreadLocal不会出现内存泄露了吧,那就是创建Thread或者Thread子类来执行任务处理,随着对应的线程Thread生命周期结束,那么线程Thread所持有的ThreadLocal也会被垃圾回收,不会出现内存泄露情况发生。
3、线程池中使用ThreadLocal是否有内存泄漏?
但是有时候我们是用线程池来执行任务的,那么在使用线程池的场景下使用ThreadLocal是否会有内存泄露的情况发生呢?
我们知道线程池里的核心Thread执行完任务之后,是不会退出的,可以循环使用,那就说明线程池里每个核心线程Thread对应的ThreadLocalMap一直是强引用关系,所以线程Thread对应的ThreadLocal是不会自动回收的。
但是细心的伙伴可能会问了,ThreadLocal是WeakReference弱引用,JDK垃圾回收的时候不是可以自动回收吗?
ThreadLocal的确是WeakReference弱引用,JDK垃圾回收的时候,也确实会回收掉弱引用的对象。
但是,伙伴们,请注意下图2左边的栈,ThreadLocal引用可是强引用,只要我们没主动释放ThreadLocal变量,它所引用的Entry中的ThreadLocal对象就不会被回收掉。
图3 内存数据模型
所以,只要我们使用的ThreadLocal变量不释放,也就是栈里的强引用一直存在,在Entry里的ThreadLocal就不会被回收,即使它是弱引用。
如果我们使用完ThreadLocal变量,手动释放ThreadLocal对象,比如把ThreadLocal对象置为null了,那么栈里对ThreadLocal对象的强引用就消失了,如果JDK垃圾回收,就会把ThreadLocal对象回收掉,Entry里的ThreadLocal的引用是弱引用,无法阻止垃圾回收。如图4所示:
图4 ThreadLocal与垃圾回收
在图4中,我们能清晰看到ThreadLocal的key是可以被自动回收变成为null,但是对应的value还是被Entry引用着呢,所以value是不能被JDK垃圾回收的。
到了这里,我想伙伴们应该知道了,在线程池场景中使用ThreadLocal是有内存泄露的可能性的,原因就是线程池的核心线程Thread是循环利用的,每个线程对应的ThreadLoalMap被强引用着,所以每个线程的ThreadLoalMap不能被回收,但是ThreadLoalMap里含有多个ThreadLocal-value的Entry,虽然ThreadLocal-key是弱引用可以被垃圾回收器自动回收,但是ThreadLocal对应的value是不能被回收的,所以说有内存泄露的情况可能性。
4、如何避免线程池中使用ThreadLocal产生内存泄漏呢?
先看下ThreadLocal源码中是如何做的。
红框1处就是获取ThreadLocal对应的Entry,然后再从Entry获取对应value,那么在红框2处,我们能看到这个if条件,如果Entry所对应的ThreadLocal被自动回收变成null了,那这个if判断条件是不成立的,就会走到getEntryAfterMiss这个方法里,我们再来看看getEntryAfterMiss这个方法的实现。
我们能够看到getEntryAfterMiss的逻辑,我们传进来的Entry e其实所对应的key,也就是ThreadLocal是为null的,所以一定会走到上图红线处这个条件里,会走到expungeStaleEntry这个方法里,我们再来看看expungeStaleEntry这个方法的逻辑。
上图红框1处,我们能清晰的看到,会把ThreadLocal为null所对应的value设置为null,同时把对应的Entry也设置为null,同时在红框2处,会遍历所有的ThreadLocal为null的value和对应的Entry都设置为null,这样就去除了强引用,有助于被垃圾回收。
到了这里,同学们可能会说,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方法,来加快垃圾回收,避免内存泄露。
5、ThreadLocal为什么使用弱引用?
ThreadLocal设置成弱引用,当堆栈里对指向threadlocal的强引用回收之后,就说明这个threadlocal就没用了,但是此时还有map中的key也指向了它,若是这个key是一个强引用,那么我们就无法对ThreadLocal进行回收,就会造成一个内存泄漏的问题,所以使用了弱引用来解决这个问题,只有弱引用指向的对象,在下次垃圾回收时就会被回收。大家可以结合图4好好理解下。
6、总结
最后简单总结一下,由于ThreadLocalMap包含了ThreadLocal,且线程Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是相同的,如果线程退出了,ThreadLocal自然就会被垃圾回收掉,所以不会出现内存泄漏。但在线程池中使用ThreadLocal的时候,我们还是要养成好习惯,ThreadLocal不在使用的时候调用remove方法,避免内存泄漏情况发生。