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

线上系统内存因为一个ThreadLocal直接飙升

时间:2023-03-19 00:41:50 科技观察

前言ThreadLocal大家应该都听说过,不知道大家是怎么掌握的。这不,我这个爱学习的表弟不知道从哪里来的听说这个技术点,回到家得意洋洋的跟我说,表弟,今天又学了一个技术点ThreadLocal,还不错,你的态度好像不靠谱,表妹咬牙切齿不不不不,我相信。我表妹那么聪明,她肯定不会做,你态度太敷衍了,不信我告诉你,不告诉我,你把Mac拿来我写somethingforyou对于她的Mac,我为她写了一个小例子0;i<1000;++i){poolExecutor.execute(newRunnable(){@Overridepublicvoidrun(){ThreadLocalthreadLocal=newThreadLocal<>();threadLocal.set(newBigObject());//其他业务代码}});Thread.sleep(1000);}}staticclassBigObject{//100Mprivatebyte[]bytes=newbyte[100*1024*1024];}}先看这段代码,说说你的我懂了表哥皱眉了。你是在侮辱我的智商吗?这不就是创建一个线程池,然后用for循环添加线程,提交一千个任务到线程池吗?任务会往ThreadLocal变量中塞入一个大对象,然后执行其他业务逻辑。简而言之,它没有任何问题。这是表弟的结论。如果你觉得这段代码还可以,那说明你对ThreadLocal的了解还不够。彻底的代码分析,让我给你一个彻底的解释。包教包会先分析上面的代码,创建一个线程池,核心线程数,最大线程数为10,保证线程池中一直有10个线程。线程正在运行。使用for循环将100个任务提交到线程池。定义了一个ThreadLocal类型的变量,Value类型是一个大对象。每个任务都会往threadLocal变量中塞一个大对象,然后执行其他的业务逻辑。由于没有调用线程池的shutdown方法,所以线程池中的线程仍然会运行。总结一下,上面的代码会造成内存泄漏,服务的内存会一直很高,即使GC之后,也不会减少太多。这不是我们想要的结果。ThreadLocal存储模型在ThreadLocal内部有一个静态内部类ThreadLocalMap,这就是真正存储对象的Map。我们平时使用的set中存储的值,其实就是存储在里面的。staticclassThreadLocalMap{//定义一个表数组,存储多个threadLocal对象及其值privateEntry[]table;ThreadLocalMap(ThreadLocalfirstKey,ObjectfirstValue){table=newEntry[INITIAL_CAPACITY];inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);table[i]=newEntry(firstKey,firstValue);size=1;setThreshold(INITIAL_CAPACITY);}//定义一个Entry类,key是弱引用的ThreadLocal对象//value是任意对象staticclassEntryextendsWeakReference>{/**这个ThreadLocal关联的值。*/Objectvalue;Entry(ThreadLocalk,Objectv){super(k);value=v;}}//省略其他,让我们关注set。如上面代码所示,ThreadLocalMap内部其实是一个Entry数组,这个Entry对象是由Key和Value组成的。关键是这个Key是ThreadLocal实例本身,这个Value是我们要存储的真实数据。看到这里是不是觉得似曾相识?是的,这个ThreadLocalMap只是一个Map。这个Map和普通Map有两点不同:1.Key,Value的存储内容不同2.ThreadLocalMap的Key是弱引用类型。其实第一点并没有什么不同,只是这里存放的Key有点出乎我们的意料。关键点在这里关键点其实是第二点,就是这个弱引用类型。首先记住,让我们看一下ThreadLocal的get和set方法来验证我的声明。ThreadLocal的set方法publicclassThreadLocal{publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}ThreadLocalMapgetMap(Threadt){返回。threadLocals;}voidcreateMap(Threadt,TfirstValue){t.threadLocals=newThreadLocalMap(this,firstValue);}//省略其他方法}set的逻辑其实很简单,获取当前线程的ThreadLocalMap,然后直接添加映射到Key和Value,这个Key就是this,this就是ThreadLocal实例本身。值是我们要存储的数据。这里需要注意。映射需要从Thread类对象中获取。看一下Thread类的定义publicclassThreadimplementsRunnable{ThreadLocal.ThreadLocalMapthreadLocals=null;//省略其他}ThreadLocal获取方法classThreadLocal{publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.entrye=map.getEntry(this);if(e!=null)return(T)e.value;}returnsetInitialValue();}}弱引用获取当前线程的ThreadLocalMap实例,如果不为空,使用currentThreadLocal直接如果ThreadLocalMap为空,或者根据当前ThreadLocal实例得到的Value为空,则执行setInitialValue(),setInitialValue内部实现是在Map不为空时设置键值对。如果为空,则创建MapThreadLocal的内部关系。这张照片非常清晰。每个Thread线程都会有一个threadlocals,也就是一个ThreadLocalMap对象。通过这个对象可以存储线程的私有变量,也就是通过ThreadLocal的set和get来操作ThreadLocal,它本身不是容器,不存储任何数据。实际存储数据的对象是一个ThreadLocalMap对象。操作过程类似于Map的put和get。这个ThreadLocalMap对象就是负责ThreadLocal真正数据存储的对象。内部存储结构为Entry数组,这个Entry是用来存放Key和Value对象的。Key是ThreadLocal实例本身,Value是我们要存储的真实数据,从上面的源码我们也看到,存储和检索是根据ThreadLocal实例操作的ThreadLocal内存模型图,栈在左边堆在右边。线程的一些局部变量和引用所使用的内存属于Stack(栈)区,而普通对象存放在Heap(堆)区。当线程运行时,我们定义的TheadLocal对象被初始化并存储在Heap中。同时线程运行的栈区保存了一个实例的引用,也就是图中的ThreadLocalRef。当调用ThreadLocal的set/get时,虚拟机根据当前线程的引用,即CurrentThreadRef,在堆区找到其对应的实例,然后检查是否创建了自己使用的TheadLocalMap实例,如果是不,创建并初始化它。Map实例化后,也获得了ThreadLocalMap的句柄,那么就可以以当前ThreadLocal对象为key进行访问操作。图中的虚线表示ThreadLocal实例对应的key的引用是弱引用。四种引用强引用,永远活着:像“Objectobj=newObject()”这样的引用,只要强引用还存在,垃圾回收器就永远不会回收被引用的对象实例。弱引用,回收会死与弱引用关联的对象实例只能存活到下一次垃圾回收发生。垃圾回收器在工作时,不管当前内存是否足够,都会回收只与弱引用关联的对象实例。JDK1.2之后提供了WeakReference类来实现弱引用。软引用有机会存活:在系统即将发生内存溢出异常之前,软引用关联的对象将被纳入回收范围进行二次回收。如果本次回收内存不足,则会抛出内存溢出异常。JDK1.2之后提供了SoftReference类来实现软引用。幻象引用,也称为幽灵引用或幽灵引用,是最弱的一类引用关系。一个对象实例是否有虚引用根本不会影响它的生命周期,并且不能通过虚引用获取对象实例。为对象设置幻影引用关联的唯一目的是在对象实例被收集器回收时接收系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。ThreadLocal中的弱引用和内存泄漏staticclassThreadLocalMap{//定义一个Entry类,key是弱引用的ThreadLocal对象//value是任意对象staticclassEntryextendsWeakReference>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;Entry(ThreadLocalk,Objectv){super(k);value=v;}}}我们先来看Entry的实现,Key会保存在WeakReference中,其中Key是key作为弱引用,我们分两种情况,当Key作为强引用时,引用ThreadLocal的对象被回收了,但是ThreadLocalMap也有对ThreadLocal的强引用,所以如果不手动删除,ThreadLocal是不会被GC回收的,会造成Entry总之,当有强引用时,需要手动删除,释放记忆键。当key作为弱引用时,引用ThreadLocal的对象被回收后,由于ThreadLocalMap持有ThreadLocal的弱引用,即使不手动删除ThreadLocal,这个ThreadLocal也会被回收,前提是该对象只关联弱引用,不能关联其他强引用!下次调用get、set、remove时会清空value,由GC自动回收。换句话说,弱引用是一个额外的障碍。当没有外部强引用时,弱引用ThreadLocal会被GC回收,但是ThreadLocal对应的Value只有在执行set、get、remove时才会被清除。比较这两种情况是由于ThreadLocalMap的生命周期和Thread的生命周期是一样的,因为它是由Thread内部实现的。如果不手动删除对应的key,会造成内存泄漏。ThreadLocal使用弱引用,这将提供额外的保护层。ThreadLocal被清理后,也会也就是Map中的key会变成null,当对应的value被使用时,value也会被清理掉。但!但!但!使用弱引用并不意味着不需要考虑内存泄漏,它只是多了一层屏障!原因内存泄漏的根本原因是:ThreadLocalMap与Thread具有相同的生命周期。如果不手动删除对应的key,对应的value不会及时清除,导致内存泄露。线程池是网上用的最多的,所以问题想一想,线程池里有10个活动线程,而且线程一直在运行,没有停止。每次直接使用线程,用完后会重新放入线程池。这个时候,线程并不会停止,也就是说这些线程的每一次使用都可能会产生一个新的ThreadLocal,而我们在使用了对应的ThreadLocal之后,如果不手动执行remove删除对应的key,Entry中的ThreadLocalMap会继续增加。而且内存永远不会被释放,这本身就是一件很可怕的事情。如果放在ThreadLocal中的对象还是超大对象,后果不堪设想。如何避免内存泄漏根据上面的分析,我们可以了解ThreadLocal内存泄漏的前因后果。那么如何避免内存泄漏呢?答案是:每次使用ThreadLocal后,建议调用其remove()方法清除数据。另外需要强调的是,并不是所有使用到ThreadLocal的地方最后都要去掉,因为它们可能需要项目的生命周期和项目的生命周期一样长,所以要做出适当的选择避免业务逻辑错误!ThreadLocal的应用场景一:ThreadLocal用于保存每个线程独享的对象,为每个线程创建一个副本,这样每个线程都可以修改自己的副本,而不影响其他线程的副本,保证线程安全。场景二:ThreadLocal用作需要在每个线程中独立保存信息的场景,以便其他方法更方便的获取信息。每个线程获取的信息可能不同。前面执行的方法保存信息后,后面的方法可以直接通过ThreadLocal获取,避免了参数传递,类似于全局变量的概念。举个具体的使用例子,可以用来保存线程不安全的工具类。需要使用的典型类是SimpleDateFormat。这样的话,每个Thread都有自己的实例副本,而这个副本只能被当前Thread访问和使用,相当于每个线程内部的一个局部变量,这也是ThreadLocal命名的意思。因为每个线程都有独占的副本,不是公用的,所以不存在多线程共享的问题。如果这种线程不安全的工具类需要在很多线程中同时使用,当任务数量巨大,也就是需要大量线程的时候,我们需要把它做成安全的,如果是不安全。比如使用Synchronized锁,这个可以解决,但是Synchronized会让线程进入排队状态,大大降低整体的工作效率。我们网上一般都是用线程池,ThreadLocal就完美了。ThreadLocal为每个线程维护自己的simpleDateFormat对象。这个对象在线程之间是独立的,相互之间没有任何关系。这也避免了线程安全问题。同时,simpleDateFormat对象也不会创建太多,线程池有多少线程,就需要多少对象。说一个生动的场景吧。每个线程都需要保存类似全局变量的信息(比如在拦截器中获取的用户信息),不同的方法可以直接使用,避免了传参的麻烦又不想被多个线程共享(因为不同的用户线程获得的信息不同)。比如使用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取的用户名、用户ID等),这些信息在同一个线程中是相同的,但是不同线程使用的业务内容是不同的.在线程生命周期中,通过静态ThreadLocal实例的get()方法获取自己设置好的对象,避免了将这个对象(如用户对象)作为参数传递的麻烦。这其实是一个类似于责任链的模型