当前位置: 首页 > 科技观察

为什么ThreadLocal容易出现内存泄露?

时间:2023-03-16 10:09:32 科技观察

本文转载自微信公众号三不猴子《三不猴》。转载本文请联系三部猴公众号。为什么ThreadLocal容易出现内存泄露?什么是线程本地?官方的解释是:该类提供线程局部变量。这些变量与它们的普通对应变量的不同之处在于,每个访问一个变量(通过其get或set方法)的线程都有其自己的、独立初始化的变量副本。我们平时创建的变量可以被任何线程访问和修改,但是用ThreadLocal创建的变量只能被当前线程访问和修改。ThreadLocal原理jdkversion1.8,我们先看看ThreadLocal的源码,先从set方法说起。/***将当前线程的此线程局部变量副本*设置为指定值。大多数子类将不需要*重写此方法,仅依赖于{@link#initialValue}*方法来设置值软线程局部变量。**@paramvalue将值存储在当前线程的*此线程局部副本中。*/publicvalue=Threadt{TcurrentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}这个ThreadLocalMap是ThreadLocal的一个内部类,key是当前的Thread对象,value是我们要保存的对象。先获取当前线程对象,然后获取一个map,然后将当前ThreadLocal对象放入map中,如果map为空,则创建一个map。看getMap的逻辑。/***GetthemaassociatedwithaThreadLocal.Overriddenin*InheritableThreadLocal.**@paramtthecurrentthread*@returnthemap*/ThreadLocalMapgetMap(Threadt){return.threadLocals;}getMap是获取Thread成员变量中的一个map。接下来是ThreadLocalMap.set(),看看set的逻辑。/***Setthevalueassociatedwithkey.**@paramkeythethreadlocalobject*@paramvaluethevaluetobeset*/privatevoidset(ThreadLocalkey,Objectvalue){//我们不使用快速路径aswithget()因为它在//leastascommontouseset()来创建新条目as//itisoreplaceexistingones,inwhichcase,afastreft/path。Entry[]tab=table;intlen=tab.length;inti=key.threadLocalHashCode&(len-1);for(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){ThreadLocalk=e.get();if(k==key){e.value=value;return;}if(k==null){replaceStaleEntry(key,value,i);return;}}tab[i]=newEntry(key,value);intsz=++size;if(!cleanSomeSlots(i,sz)&&sz>=threshold)rehash();}这里构造了一个Entry对象,这个Entry就可以了可以看成一行map数据,一个key-valuepair。查看Entry的源代码。staticclassEntryextendsWeakReference>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;Entry(ThreadLocalk,Objectv){super(k);value=v;}}这个Entry对象其实继承了WeakReference对象。上面的过程就是这样用图画出来的。总结一下:每个Thread都维护一个对ThreadLocalMap的引用。ThreadLocalMap是ThreadLocal的一个内部类。在使用Entry存储和调用ThreadLocal的set()方法时,实际上是将值设置到ThreadLocalMap中。key是ThreadLocal对象,传值是传入对象调用ThreadLocal的get()方法时,实际上是从ThreadLocalMap中获取值。关键是ThreadLocal对象。ThreadLocal本身不存储值,它只是作为一个键,让线程从ThreadLocalMap中获取值。什么是弱引用?为什么ThreadLocal使用弱引用?官方文档解释:/***Weakreferenceobjects,whichdoesnotpreventtheirreferentsbeing*madefinalizable,finalized,andthenreclaimed。-summary.html#reachability">weakly*reachable.Atthattimeitwillatomicallyclearallweakreferencesto*thatobjectandallweakreferencestoanyotherweakly-reachableobjects*fromwhichthatobjectisreachablethroughachainofstrongandsoft*references.Atthesametimeitwilldeclarealloftheformerly*weakly-reachableobjectstobefinalizable.Atthesametimeoratsome*latertimeitwillenqueuethosenewly-clearedweakreferencesthatare*registeredwithreferencequeues.**@authorMarkReinhold*@since1.2*/是一个引用,不会被程序计数器统计,所以垃圾回收器回收的时候不管有没有引用都会被回收,由于垃圾回收器是一个优先级很低的线程,不一定会找到对象只有弱引用很快。为什么ThreadLocal使用弱引用?因为当我们存储的对象设置为null时,即ThreadLocalMap的值为null,而ThreadLocalMap的key是弱引用。这时候就可以在下一个垃圾收集器回收垃圾的时候回收了。这个键值也是一个Entry对象。既然弱引用有利于垃圾回收,为什么ThreadLocal仍然容易出现内存泄漏?弱引用确实对垃圾回收有帮助,但也有缺点。假设我们现在存了一个对象,虚拟机gc了,key弱引用被回收了,但是value还是强引用,key被回收了。这个值是无法通过ThreadLocal对象的get方法获取到的,永远也访问不到,所以存在内存泄漏的风险。如何避免内存泄露在ThreadLocal使用前后调用remove清理,异常情况在finally中清理。尽量不要使用全局的ThreadLocal。静态变量的生命周期与类的生命周期是一致的,类的卸载时机可以说是苛刻的。这将导致静态ThreadLocal被垃圾回收并容易出现内存泄漏。