ThreadLocal中的3个大坑,内存泄露都是小儿科!
我在參加Code Review的時(shí)候不止一次聽到有同學(xué)說(shuō):我寫的這個(gè)上下文工具沒(méi)問(wèn)題,在線上跑了好久了。其實(shí)這種想法是有問(wèn)題的,ThreadLocal寫錯(cuò)難,但是用錯(cuò)就很容易,本文將會(huì)詳細(xì)總結(jié)ThreadLocal容易用錯(cuò)的三個(gè)坑:
內(nèi)存泄露
線程池中線程上下文丟失
并行流中線程上下文丟失
內(nèi)存泄露
由于ThreadLocal的key是弱引用,因此如果使用后不調(diào)用remove清理的話會(huì)導(dǎo)致對(duì)應(yīng)的value內(nèi)存泄露。
@Test public?void?testThreadLocalMemoryLeaks()?{ThreadLocal<List<Integer>>?localCache?=?new?ThreadLocal<>();List<Integer>?cacheInstance?=?new?ArrayList<>(10000);localCache.set(cacheInstance);localCache?=?new?ThreadLocal<>(); }當(dāng)localCache的值被重置之后cacheInstance被ThreadLocalMap中的value引用,無(wú)法被GC,但是其key對(duì)ThreadLocal實(shí)例的引用是一個(gè)弱引用,本來(lái)ThreadLocal的實(shí)例被localCache和ThreadLocalMap的key同時(shí)引用,但是當(dāng)localCache的引用被重置之后,則ThreadLocal的實(shí)例只有ThreadLocalMap的key這樣一個(gè)弱引用了,此時(shí)這個(gè)實(shí)例在GC的時(shí)候能夠被清理。
其實(shí)看過(guò)ThreadLocal源碼的同學(xué)會(huì)知道,ThreadLocal本身對(duì)于key為null的Entity有自清理的過(guò)程,但是這個(gè)過(guò)程是依賴于后續(xù)對(duì)ThreadLocal的繼續(xù)使用,假如上面的這段代碼是處于一個(gè)秒殺場(chǎng)景下,會(huì)有一個(gè)瞬間的流量峰值,這個(gè)流量峰值也會(huì)將集群的內(nèi)存打到高位(或者運(yùn)氣不好的話直接將集群內(nèi)存打滿導(dǎo)致故障),后面由于峰值流量已過(guò),對(duì)ThreadLocal的調(diào)用也下降,會(huì)使得ThreadLocal的自清理能力下降,造成內(nèi)存泄露。ThreadLocal的自清理是錦上添花,千萬(wàn)不要指望他雪中送碳。
相比于ThreadLocal中存儲(chǔ)的value對(duì)象泄露,ThreadLocal用在web容器中時(shí)更需要注意其引起的ClassLoader泄露。
Tomcat官網(wǎng)對(duì)在web容器中使用ThreadLocal引起的內(nèi)存泄露做了一個(gè)總結(jié),詳見:https://cwiki.apache.org/confluence/display/tomcat/MemoryLeakProtection,這里我們列舉其中的一個(gè)例子。
熟悉Tomcat的同學(xué)知道,Tomcat中的web應(yīng)用由Webapp Classloader這個(gè)類加載器的,并且Webapp Classloader是破壞雙親委派機(jī)制實(shí)現(xiàn)的,即所有的web應(yīng)用先由Webapp classloader加載,這樣的好處就是可以讓同一個(gè)容器中的web應(yīng)用以及依賴隔離。
下面我們看具體的內(nèi)存泄露的例子:
public?class?MyCounter?{private?int?count?=?0;public?void?increment()?{count++;}public?int?getCount()?{return?count;} }public?class?MyThreadLocal?extends?ThreadLocal<MyCounter>?{ }public?class?LeakingServlet?extends?HttpServlet?{private?static?MyThreadLocal?myThreadLocal?=?new?MyThreadLocal();protected?void?doGet(HttpServletRequest?request,HttpServletResponse?response)?throws?ServletException,?IOException?{MyCounter?counter?=?myThreadLocal.get();if?(counter?==?null)?{counter?=?new?MyCounter();myThreadLocal.set(counter);}response.getWriter().println("The?current?thread?served?this?servlet?"?+?counter.getCount()+?"?times");counter.increment();} }需要注意這個(gè)例子中的兩個(gè)非常關(guān)鍵的點(diǎn):
MyCounter以及MyThreadLocal必須放到web應(yīng)用的路徑中,保被Webapp Classloader加載
ThreadLocal類一定得是ThreadLocal的繼承類,比如例子中的MyThreadLocal,因?yàn)門hreadLocal本來(lái)被Common Classloader加載,其生命周期與Tomcat容器一致。ThreadLocal的繼承類包括比較常見的NamedThreadLocal,注意不要踩坑。
假如LeakingServlet所在的Web應(yīng)用啟動(dòng),MyThreadLocal類也會(huì)被Webapp Classloader加載,如果此時(shí)web應(yīng)用下線,而線程的生命周期未結(jié)束(比如為L(zhǎng)eakingServlet提供服務(wù)的線程是一個(gè)線程池中的線程),那會(huì)導(dǎo)致myThreadLocal的實(shí)例仍然被這個(gè)線程引用,而不能被GC,期初看來(lái)這個(gè)帶來(lái)的問(wèn)題也不大,因?yàn)閙yThreadLocal所引用的對(duì)象占用的內(nèi)存空間不太多,問(wèn)題在于myThreadLocal間接持有加載web應(yīng)用的webapp classloader的引用(通過(guò)myThreadLocal.getClass().getClassLoader()可以引用到),而加載web應(yīng)用的webapp classloader有持有它加載的所有類的引用,這就引起了Classloader泄露,它泄露的內(nèi)存就非常可觀了。
線程池中線程上下文丟失
ThreadLocal不能在父子線程中傳遞,因此最常見的做法是把父線程中的ThreadLocal值拷貝到子線程中,因此大家會(huì)經(jīng)常看到類似下面的這段代碼:
for(value?in?valueList){Future<?>?taskResult?=?threadPool.submit(new?BizTask(ContextHolder.get()));//提交任務(wù),并設(shè)置拷貝Context到子線程results.add(taskResult); } for(result?in?results){result.get();//阻塞等待任務(wù)執(zhí)行完成 }提交的任務(wù)定義長(zhǎng)這樣:
class?BizTask<T>?implements?Callable<T>??{private?String?session?=?null;public?BizTask(String?session)?{this.session?=?session;}@Overridepublic?T?call(){try?{ContextHolder.set(this.session);//?執(zhí)行業(yè)務(wù)邏輯}?catch(Exception?e){//log?error}?finally?{ContextHolder.remove();?//?清理?ThreadLocal?的上下文,避免線程復(fù)用時(shí)context互串}return?null;} }對(duì)應(yīng)的線程上下文管理類為:
class?ContextHolder?{private?static?ThreadLocal<String>?localThreadCache?=?new?ThreadLocal<>();public?static?void?set(String?cacheValue)?{localThreadCache.set(cacheValue);}public?static?String?get()?{return?localThreadCache.get();}public?static?void?remove()?{localThreadCache.remove();}}這么寫倒也沒(méi)有問(wèn)題,我們?cè)倏纯淳€程池的設(shè)置:
ThreadPoolExecutor?executorPool?=?new?ThreadPoolExecutor(20,?40,?30,?TimeUnit.SECONDS,?new?LinkedBlockingQueue<Runnable>(40),?new?XXXThreadFactory(),?ThreadPoolExecutor.CallerRunsPolicy);其中最后一個(gè)參數(shù)控制著當(dāng)線程池滿時(shí),該如何處理提交的任務(wù),內(nèi)置有4種策略
ThreadPoolExecutor.AbortPolicy?//直接拋出異常 ThreadPoolExecutor.DiscardPolicy?//丟棄當(dāng)前任務(wù) ThreadPoolExecutor.DiscardOldestPolicy?//丟棄工作隊(duì)列頭部的任務(wù) ThreadPoolExecutor.CallerRunsPolicy?//轉(zhuǎn)串行執(zhí)行可以看到,我們初始化線程池的時(shí)候指定如果線程池滿,則新提交的任務(wù)轉(zhuǎn)為串行執(zhí)行,那我們之前的寫法就會(huì)有問(wèn)題了,串行執(zhí)行的時(shí)候調(diào)用ContextHolder.remove();會(huì)將主線程的上下文也清理,即使后面線程池繼續(xù)并行工作,傳給子線程的上下文也已經(jīng)是null了,而且這樣的問(wèn)題很難在預(yù)發(fā)測(cè)試的時(shí)候發(fā)現(xiàn)。
并行流中線程上下文丟失
如果ThreadLocal碰到并行流,也會(huì)有很多有意思的事情發(fā)生,比如有下面的代碼:
class?ParallelProcessor<T>?{public?void?process(List<T>?dataList)?{//?先校驗(yàn)參數(shù),篇幅限制先省略不寫dataList.parallelStream().forEach(entry?->?{doIt();});}private?void?doIt()?{String?session?=?ContextHolder.get();//?do?something} }這段代碼很容易在線下測(cè)試的過(guò)程中發(fā)現(xiàn)不能按照預(yù)期工作,因?yàn)椴⑿辛鞯讓拥膶?shí)現(xiàn)也是一個(gè)ForkJoin線程池,既然是線程池,那ContextHolder.get()可能取出來(lái)的就是一個(gè)null。我們順著這個(gè)思路把代碼再改一下:
class?ParallelProcessor<T>?{private?String?session;public?ParallelProcessor(String?session)?{this.session?=?session;}public?void?process(List<T>?dataList)?{//?先校驗(yàn)參數(shù),篇幅限制先省略不寫dataList.parallelStream().forEach(entry?->?{try?{ContextHolder.set(session);//?業(yè)務(wù)處理doIt();}?catch?(Exception?e)?{//?log?it}?finally?{ContextHolder.remove();}});}private?void?doIt()?{String?session?=?ContextHolder.get();//?do?something} }修改完后的這段代碼可以工作嗎?如果運(yùn)氣好,你會(huì)發(fā)現(xiàn)這樣改又有問(wèn)題,運(yùn)氣不好,這段代碼在線下運(yùn)行良好,這段代碼就順利上線了。不久你就會(huì)發(fā)現(xiàn)系統(tǒng)中會(huì)有一些其他很詭異的bug。原因在于并行流的設(shè)計(jì)比較特殊,父線程也有可能參與到并行流線程池的調(diào)度,那如果上面的process方法被父線程執(zhí)行,那么父線程的上下文會(huì)被清理。導(dǎo)致后續(xù)拷貝到子線程的上下文都為null,同樣產(chǎn)生丟失上下文的問(wèn)題。
往期推薦額!Java中用戶線程和守護(hù)線程區(qū)別這么大?
線程的故事:我的3位母親成就了優(yōu)秀的我!
Semaphore自白:限流器用我就對(duì)了!
CyclicBarrier:人齊了,老司機(jī)就發(fā)車了!
總結(jié)
以上是生活随笔為你收集整理的ThreadLocal中的3个大坑,内存泄露都是小儿科!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ruby 将字符转数字计算_Ruby程序
- 下一篇: notepad++ 偶数行_C ++程序