ThreadLocal源码分析-黄金分割数的使用 (6)

ThreadLocal源码分析-黄金分割数的使用

TreadLocal的remove方法

ThreadLocal中remove()方法的源码如下:

public void remove() { //获取Thread实例中的ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //根据当前ThreadLocal作为Key对ThreadLocalMap的元素进行移除 m.remove(this); }

ThreadLocal源码分析-黄金分割数的使用

ThreadLocal.ThreadLocalMap的初始化

我们可以关注一下java.lang.Thread类里面的变量:

public class Thread implements Runnable { //传递ThreadLocal中的ThreadLocalMap变量 ThreadLocal.ThreadLocalMap threadLocals = null; //传递InheritableThreadLocal中的ThreadLocalMap变量 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; }

也就是,ThreadLocal需要存放和获取的数据实际上绑定在Thread实例的成员变量threadLocals中,并且是ThreadLocal#set()方法调用的时候才进行懒加载的,可以结合上一节的内容理解一下,这里不展开。

什么情况下ThreadLocal的使用会导致内存泄漏

其实ThreadLocal本身不存放任何的数据,而ThreadLocal中的数据实际上是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是Thread对象中的成员变量threadLocals持有大量的K-V结构,并且线程一直处于活跃状态导致变量threadLocals无法释放被回收。threadLocals持有大量的K-V结构这一点的前提是要存在大量的ThreadLocal实例的定义,一般来说,一个应用不可能定义大量的ThreadLocal,所以一般的泄漏源是线程一直处于活跃状态导致变量threadLocals无法释放被回收。但是我们知道,·ThreadLocalMap·中的Entry结构的Key用到了弱引用(·WeakReference<ThreadLocal<?>>·),当没有强引用来引用ThreadLocal实例的时候,JVM的GC会回收ThreadLocalMap中的这些Key,此时,ThreadLocalMap中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。也就是为什么ThreadLocal中get()、set()、remove()这些方法中都存在清理ThreadLocalMap实例key为null的代码块。总结下来,内存泄漏可能出现的地方是:

1、大量地(静态)初始化ThreadLocal实例,初始化之后不再调用get()、set()、remove()方法。

2、初始化了大量的ThreadLocal,这些ThreadLocal中存放了容量大的Value,并且使用了这些ThreadLocal实例的线程一直处于活跃的状态。

ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap中的所有数据都是与Thread的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。

其实ThreadLocal在设置内部类ThreadLocal.ThreadLocalMap中构建的Entry哈希表已经考虑到内存泄漏的问题,所以ThreadLocal.ThreadLocalMap$Entry类设计为弱引用,类签名为static class Entry extends WeakReference<ThreadLocal<?>>。之前一篇文章介绍过,如果弱引用关联的对象如果置为null,那么该弱引用会在下一次GC时候回收弱引用关联的对象。举个例子:

public class ThreadLocalMain { private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>(); public static void main(String[] args) throws Exception { TL_1.set(1); TL_1 = null; System.gc(); Thread.sleep(300); } }

这种情况下,TL_1这个ThreadLocal在主动GC之后,线程绑定的ThreadLocal.ThreadLocalMap实例中的Entry哈希表中原来的TL_1所在的哈希槽Entry的引用持有值referent(继承自WeakReference)会变成null,但是Entry中的value是强引用,还存放着TL_1这个ThreadLocal未回收之前的值。这些被"孤立"的哈希槽Entry就是前面说到的要惰性删除的哈希槽。

ThreadLocal的最佳实践

其实ThreadLocal的最佳实践很简单:

每次使用完ThreadLocal实例,都调用它的remove()方法,清除Entry中的数据。

调用remove()方法最佳时机是线程运行结束之前的finally代码块中调用,这样能完全避免操作不当导致的内存泄漏,这种主动清理的方式比惰性删除有效。

父子线程数据传递InheritableThreadLocal

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zywdgj.html