
写在前面本文看下threadlocal相关内容。1使用场景以及源码分析1.1使用场景当方法调用链比较长、需要某个值在整个调用链中可用时通过方法入参来传递自然是可以的但是不免过于麻烦。此时我们就可以考虑使用threadlocal了可以定义一个和线程绑定的变量随用随取像这样:packagecom.demo.xx;publicclassLoadByClassLoader{staticThreadLocalStringthreadLocalnewThreadLocal();publicstaticvoidmain(String[]args)throwsException{threadLocal.set(aaaaaaaa);m1();}privatestaticvoidm1(){System.out.println(LoadByClassLoader.m1: threadLocal.get());m2();}privatestaticvoidm2(){System.out.println(LoadByClassLoader.m2: threadLocal.get());}}运行[INFO] --- exec:3.6.3:exec (default-cli) syncLockTest --- LoadByClassLoader.m1: aaaaaaaa LoadByClassLoader.m2: aaaaaaaa [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------1.2源码分析调用set方法//java.lang.ThreadLocal.setpublicvoidset(Tvalue){ThreadtThread.currentThread();// 是thread的一个成员变量java.lang.Thread.threadLocalsThreadLocalMapmapgetMap(t);if(map!null)map.set(this,value);elsecreateMap(t,value);}从代码map.set(this, value);可以看到出map的key是我们定义的threadlocal所以我们可以定义多个threadlocal来实现多个变量的传递。map.set(this, value);源码如下// java.lang.ThreadLocal.ThreadLocalMap.setprivatevoidset(ThreadLocal?key,Objectvalue){// ...tab[i]newEntry(key,value);// ...}就是放到entry数组中没什么这里的重点其实是entry源码如下// java.lang.ThreadLocal.ThreadLocalMap.EntrystaticclassEntryextendsWeakReferenceThreadLocal?{/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal?k,Objectv){super(k);valuev;}}是ThreadLocalMap的一个内部类关键是Entry继承了WeakReference所以是一个弱引用类看构造函数中super(k);这里的k是通过一个弱引用来引用的也就是说threadlocal在entry中是通过弱引用来引用的即结构如下图虚线就是弱引用。2为什么会出现内存泄漏如何避免内存泄露2.1为什么会出现内存泄漏我们使用threadlocal后引用关系如下图根据可达性分析可以看到有两条线路是对threadlocalmap可达的所以当threadlocal使用完毕比如执行了threadlocalnull变为如下结构时此时如果是发生了gc因为entry的key对于堆中threadlocal对象是弱引用所以可以被顺利回收但是value因为还有如下的强引用存在所以是不能被回收的这个时候就发生内存泄漏了。2.2如何避免内存泄露养成好习惯不使用了调用remove方法。3出现内存泄漏的根本原因分析根本原因是threadlocalmap是thread的成员变量所以其生命周期是和thread对象绑定的其生命周期可能很长特别是在像线程池这样的场景中。要格外注意出现问题。这里多说一点为什么entry的key要使用弱引用呢还是因为threadlocalmap生命周期太长如果使用强引用的话将将可能导致threadlocal也无法回收此时entry中的threadlocal和value同时会发生内存泄漏了。所以这种设计的目的是尽量减少内存泄漏的风险。所以到这里可以看到发生内存泄漏不能让虚引用背锅相反的虚引用还是为内存泄漏做了一定贡献的。4但真的是这样吗按照上面的分析如果是发生了这种情况内存就应该一路走高了最终内存溢出了但真的是这样吗使用如下程序验证没有调用remove清除valuepackagecom.demo.xx;publicclassLoadByClassLoader{publicstaticvoidmain(String[]args)throwsException{newThread(newRunnable(){Overridepublicvoidrun(){for(inti0;i20000;i){System.out.println(i);ThreadLocalByte[]localVariablenewThreadLocalByte[]();localVariable.set(newByte[4096*1024]);// 为线程添加变量localVariablenull;}}}).start();}}jvm配置-Dfile.encodingutf-8 -Xms2000m -Xmx4000m,观察堆内存变化实际上并非我们所预想的那样一路走高而是有很明显的降低即是被回收了的这又是为什么呢这是因为Entry是一个弱引用的子类引用路径如下thread-threadlocalmap-entry(弱引用)-value(即4M的数组)因为这里entry是弱引用而key因为是虚引用已经是null了所以在语义上value(即4M的数组)在语义上已经是不可达的了此时jvm会将entry标记为可回收的潜在对象是会进行回收的但是这种回收也是非常不靠谱的从堆内存最高占用已经到了2.5G就可以看出来了所以还是有很高的风险会造成内存溢出也所以最靠谱的方案依然是在finally中执行remove那么如果这样做了堆内存占用又是怎样的呢来看下packagecom.demo.xx;publicclassLoadByClassLoader{publicstaticvoidmain(String[]args)throwsException{newThread(newRunnable(){Overridepublicvoidrun(){// 这里次数增大10倍因为不会有频繁的gc导致程序很快执行结束不好观察内存变化情况for(inti0;i200000;i){ThreadLocalByte[]localVariablenewThreadLocalByte[]();try{System.out.println(i);localVariable.set(newByte[4096*1024]);// 为线程添加变量// localVariable null;}finally{localVariable.remove();localVariablenull;}}}}).start();}}可看到此时峰值在1.5G,并且gc后堆内存的回收效果也很好可以看到一行代码的影响力有多大了。好的编程习惯真的是太重要了写在后面参考文章列表ThreadLocal的内存泄露什么原因如何避免 。