转载本文请联系LoveSmile的架构师公众号。ThreadLocal的使用不规范,一个实习生来到master的两行眼泪组。看着这个满脸幸福、精神抖擞、头发稀疏的小伙子,我心中一喜:绝对是个潜力股。于是我让经理亲自申请带他来。为了帮助小伙子快速成长,我给他分配了一个需求。这不需要刚上线几天就出现上线问题。后台监控服务发现内存一直在慢慢增加。初步怀疑是内存泄漏。把所有实习生的PR都找出来,仔细审核,果然有问题。由于公司内部代码保密,这里给出一个简单的demo还原场景(忽略代码风格问题)。publicclassThreadPoolDemo{privatestaticfinalThreadPoolExecutorpoolExecutor=newThreadPoolExecutor(5,5,1,TimeUnit.MINUTES,newLinkedBlockingQueue<>());publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<100;++i){poolExecutor。executor(newRunnable(){@Overridepublicvoidrun(){ThreadLocalthreadLocal=newThreadLocal<>();threadLocal.set(newBigObject());//其他业务代码}});Thread.sleep(1000);}}staticclassBigObject{//100Mprivatebyte[]bytes=newbyte[100*1024*1024];}}代码分析:创建一个线程池,核心线程数为10个,最大线程数为10,保证始终有10个线程在运行线程池。使用for循环将100个任务提交到线程池。定义了一个ThreadLocal类型的变量,Value类型是一个大对象。每个任务都会往threadLocal变量中塞一个大对象,然后执行其他的业务逻辑。由于没有调用线程池的shutdown方法,所以线程池中的线程仍然会运行。乍一看代码好像没什么问题,那为什么服务完GC后内存一直很高呢?代码中给threadLocal赋值了一个大对象,但是执行完业务逻辑后并没有调用remove方法,最终导致线程池出现问题10个线程的threadLocals变量中包含的大对象没有被释放,并发生内存泄漏。请问这样的实习生能不能留下来?ThreadLocal的价值存在于何处?实习生说他以为threadLocal分配的对象在线程任务结束后会被JVM垃圾回收,想不通为什么会出现内存泄漏。身为高手,我一定要把道理讲给他听。ThreadLocal类提供了set/get方法来存储和获取value值,但实际上ThreadLocal类并不存储value值。真正的存储取决于ThreadLocalMap类。ThreadLocalMap是ThreadLocal的静态内部类。它的key是ThreadLocal实例对象,value是任意的Object对象。ThreadLocalMap类的定义staticclassThreadLocalMap{//定义一个表数组,用于存储多个threadLocal对象及其值-1);table[i]=newEntry(firstKey,firstValue);size=1;setThreshold(INITIAL_CAPACITY);}//定义一个Entry类,key是弱引用的ThreadLocal对象//value是任意对象staticclassEntryextendsWeakReference>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;Entry(ThreadLocal>k,Objectv){super(k);value=v;}}//省略其他}进一步分析代码ThreadLocal类,看set和get方法是如何关联ThreadLocalMap静态内部类的。ThreadLocal类设置方法publicclassThreadLocal{publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}ThreadLocalMapgetMap(Threadt){return.threadLocals;}voidcreateMap(Threadt,TfirstValue){t.threadLocals=newThreadLocalMap(this,firstValue);}//省略其他方法}set的逻辑比较简单,就是获取当前线程的ThreadLocalMap,然后在map中添加KV,K是当前ThreadLocal实例,V是我们传入的值。这里需要注意的是,map的获取需要从Thread中获取类对象。看一下Thread类的定义。publicclassThreadimplementsRunnable{ThreadLocal.ThreadLocalMapthreadLocals=null;//省略其他}Thread类维护了一个ThreadLocalMap的变量引用。ThreadLocal类的get方法get获取当前线程对应的私有变量,也就是之前设置的值或者initialValue,代码如下:classThreadLocal{publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null)return(T)e.value;}returnsetInitialValue();}}代码逻辑分析:获取当前线程实例;如果不为空,则使用当前ThreadLocal实例作为key获取值;如果ThreadLocalMap为空或者根据当前ThreadLocal实例获取的值为空,则执行setInitialValue();ThreadLocal相关类的关系看完上面的分析就总结出来了。我对Thread、ThreadLocal、ThreadLocalMap和Entry之间的关系有点困惑。没关系。我画了一个UML类图来总结一下(忽略UML标准语法)。ThreadLocal相关类之间的关系每个线程都是一个Thread实例,其内部维护了一个threadLocals的实例成员,其类型为ThreadLocal.ThreadLocalMap。通过实例化一个ThreadLocal实例,我们可以为当前运行的线程设置一些线程私有变量,并通过调用ThreadLocal的set和get方法来访问它们。ThreadLocal本身不是容器。我们访问的值其实是存储在ThreadLocalMap中的,而ThreadLocal只是作为TheadLocalMap的key。每个线程实例对应一个TheadLocalMap实例。我们可以在同一个线程中实例化多个ThreadLocal来存储各种类型的值。这些ThreadLocal实例作为键,对应各自的值,最终存储在Entry表数组中。在调用ThreadLocal的set/get进行赋值/取值操作时,先获取当前线程的ThreadLocalMap实例,然后像操作普通map一样进行put和get。ThreadLocal内存模型的原理经过上面的分析,我们对ThreadLocal相关的类设计有了一个非常清晰的认识。下面通过一张图来深入了解一下ThreadLocal的内存存储。在ThreadLocal内存模型图中,左边是栈,右边是堆。线程的一些局部变量和引用所使用的内存属于Stack(栈)区,而普通对象存放在Heap(堆)区。当线程运行时,我们定义的TheadLocal对象被初始化并存储在Heap中。同时线程运行的栈区保存了一个实例的引用,也就是图中的ThreadLocalRef。当调用ThreadLocal的set/get时,虚拟机根据当前线程的引用,即CurrentThreadRef,在堆区找到其对应的实例,然后检查是否创建了自己使用的TheadLocalMap实例,如果是不,创建并初始化它。Map实例化后,也获得了ThreadLocalMap的句柄,那么就可以以当前ThreadLocal对象为key进行访问操作。图中的虚线表示ThreadLocal实例对应的key的引用是弱引用。强引用和弱引用的概念ThreadLocalMap的key是弱引用类型,源码如下:staticclassThreadLocalMap{//定义一个Entry类,key是弱引用的ThreadLocal对象//value是任意对象staticclassEntryextendsWeakReference>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;Entry(ThreadLocal>k,Objectv){super(k);value=v;}}//省略其他}下面解释几个常见的参考概念。强引用永远活着:对于像“Objectobj=newObject()”这样的引用,只要强引用还存在,垃圾回收器就永远不会回收被引用的对象实例。弱引用收集就是死亡:与弱引用关联的对象实例只能存活到下一次垃圾收集发生。垃圾回收器在工作时,不管当前内存是否足够,都会回收只与弱引用关联的对象实例。JDK1.2之后提供了WeakReference类来实现弱引用。软引用有机会存活:在系统即将发生内存溢出异常之前,软引用关联的对象将被纳入回收范围进行二次回收。如果本次回收内存不足,则会抛出内存溢出异常。JDK1.2之后提供了SoftReference类来实现软引用。幻象引用,也称为幽灵引用或幽灵引用,是最弱的一类引用关系。一个对象实例是否有虚引用根本不会影响它的生命周期,并且不能通过虚引用获取对象实例。为对象设置幻影引用关联的唯一目的是在对象实例被收集器回收时接收系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。内存泄漏是弱引用的锅?从表面上看,内存泄漏的根源是弱引用的使用,但另一个问题也值得思考:为什么ThreadLocalMap使用弱引用而不是强引用?查看官方网站文档:为了帮助处理非常大且长期存在的用法,哈希表条目使用Wea??kReferences作为键。分两种情况讨论:(1)key使用强引用引用ThreadLocal对象被回收,但是ThreadLocalMap仍然持有对ThreadLocal的强引用。如果不手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。(2)key使用弱引用引用ThreadLocal的对象被回收。由于ThreadLocalMap持有ThreadLocal的弱引用,即使不手动删除,ThreadLocal也会被回收。下次ThreadLocalMap调用set、get和remove时,该值将被清除。对比两种情况,我们可以发现:由于ThreadLocalMap的生命周期和Thread一样长,如果不手动删除对应的key,会造成内存泄漏,但是使用弱引用可以多一层保护:弱引用ThreadLocal清理后,key为null,下次ThreadLocalMap调用set、get、remove时可能会清理对应的value。因此,ThreadLocal内存泄漏的根本原因是:由于ThreadLocalMap的生命周期与Thread一样长,如果不手动删除对应的key,就会造成内存泄漏,并不是因为弱引用。ThreadLocalBestPractice通过前面的章节,我们分析了ThreadLocal的类设计和内存模型,也着重分析了内存泄漏发生的条件和具体场景。最后结合项目中的经验,推荐使用ThreadLocal的场景:需要存储线程私有变量的时候。当您需要实现线程安全变量时。当需要减少线程资源竞争时。基于上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么如何避免内存泄漏呢?答案是:每次使用ThreadLocal时,建议调用其remove()方法清除数据。另外需要强调的是,并不是所有用到ThreadLocal的地方最后都要remove(),因为它们的生命周期可能需要和项目的生命周期一样长,所以要适当的选择,避免业务逻辑错误!