日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

ThreadLocal中的3个大坑,内存泄露都是小儿科!

發布時間:2025/3/11 编程问答 21 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ThreadLocal中的3个大坑,内存泄露都是小儿科! 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

我在參加Code Review的時候不止一次聽到有同學說:我寫的這個上下文工具沒問題,在線上跑了好久了。其實這種想法是有問題的,ThreadLocal寫錯難,但是用錯就很容易,本文將會詳細總結ThreadLocal容易用錯的三個坑:

  • 內存泄露

  • 線程池中線程上下文丟失

  • 并行流中線程上下文丟失

  • 內存泄露

    由于ThreadLocal的key是弱引用,因此如果使用后不調用remove清理的話會導致對應的value內存泄露。

    @Test public?void?testThreadLocalMemoryLeaks()?{ThreadLocal<List<Integer>>?localCache?=?new?ThreadLocal<>();List<Integer>?cacheInstance?=?new?ArrayList<>(10000);localCache.set(cacheInstance);localCache?=?new?ThreadLocal<>(); }

    當localCache的值被重置之后cacheInstance被ThreadLocalMap中的value引用,無法被GC,但是其key對ThreadLocal實例的引用是一個弱引用,本來ThreadLocal的實例被localCache和ThreadLocalMap的key同時引用,但是當localCache的引用被重置之后,則ThreadLocal的實例只有ThreadLocalMap的key這樣一個弱引用了,此時這個實例在GC的時候能夠被清理。

    其實看過ThreadLocal源碼的同學會知道,ThreadLocal本身對于key為null的Entity有自清理的過程,但是這個過程是依賴于后續對ThreadLocal的繼續使用,假如上面的這段代碼是處于一個秒殺場景下,會有一個瞬間的流量峰值,這個流量峰值也會將集群的內存打到高位(或者運氣不好的話直接將集群內存打滿導致故障),后面由于峰值流量已過,對ThreadLocal的調用也下降,會使得ThreadLocal的自清理能力下降,造成內存泄露。ThreadLocal的自清理是錦上添花,千萬不要指望他雪中送碳。

    相比于ThreadLocal中存儲的value對象泄露,ThreadLocal用在web容器中時更需要注意其引起的ClassLoader泄露。

    Tomcat官網對在web容器中使用ThreadLocal引起的內存泄露做了一個總結,詳見:https://cwiki.apache.org/confluence/display/tomcat/MemoryLeakProtection,這里我們列舉其中的一個例子。

    熟悉Tomcat的同學知道,Tomcat中的web應用由Webapp Classloader這個類加載器的,并且Webapp Classloader是破壞雙親委派機制實現的,即所有的web應用先由Webapp classloader加載,這樣的好處就是可以讓同一個容器中的web應用以及依賴隔離。

    下面我們看具體的內存泄露的例子:

    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();} }

    需要注意這個例子中的兩個非常關鍵的點:

    • MyCounter以及MyThreadLocal必須放到web應用的路徑中,保被Webapp Classloader加載

    • ThreadLocal類一定得是ThreadLocal的繼承類,比如例子中的MyThreadLocal,因為ThreadLocal本來被Common Classloader加載,其生命周期與Tomcat容器一致。ThreadLocal的繼承類包括比較常見的NamedThreadLocal,注意不要踩坑。

    假如LeakingServlet所在的Web應用啟動,MyThreadLocal類也會被Webapp Classloader加載,如果此時web應用下線,而線程的生命周期未結束(比如為LeakingServlet提供服務的線程是一個線程池中的線程),那會導致myThreadLocal的實例仍然被這個線程引用,而不能被GC,期初看來這個帶來的問題也不大,因為myThreadLocal所引用的對象占用的內存空間不太多,問題在于myThreadLocal間接持有加載web應用的webapp classloader的引用(通過myThreadLocal.getClass().getClassLoader()可以引用到),而加載web應用的webapp classloader有持有它加載的所有類的引用,這就引起了Classloader泄露,它泄露的內存就非常可觀了。

    線程池中線程上下文丟失

    ThreadLocal不能在父子線程中傳遞,因此最常見的做法是把父線程中的ThreadLocal值拷貝到子線程中,因此大家會經常看到類似下面的這段代碼:

    for(value?in?valueList){Future<?>?taskResult?=?threadPool.submit(new?BizTask(ContextHolder.get()));//提交任務,并設置拷貝Context到子線程results.add(taskResult); } for(result?in?results){result.get();//阻塞等待任務執行完成 }

    提交的任務定義長這樣:

    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);//?執行業務邏輯}?catch(Exception?e){//log?error}?finally?{ContextHolder.remove();?//?清理?ThreadLocal?的上下文,避免線程復用時context互串}return?null;} }

    對應的線程上下文管理類為:

    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();}}

    這么寫倒也沒有問題,我們再看看線程池的設置:

    ThreadPoolExecutor?executorPool?=?new?ThreadPoolExecutor(20,?40,?30,?TimeUnit.SECONDS,?new?LinkedBlockingQueue<Runnable>(40),?new?XXXThreadFactory(),?ThreadPoolExecutor.CallerRunsPolicy);

    其中最后一個參數控制著當線程池滿時,該如何處理提交的任務,內置有4種策略

    ThreadPoolExecutor.AbortPolicy?//直接拋出異常 ThreadPoolExecutor.DiscardPolicy?//丟棄當前任務 ThreadPoolExecutor.DiscardOldestPolicy?//丟棄工作隊列頭部的任務 ThreadPoolExecutor.CallerRunsPolicy?//轉串行執行

    可以看到,我們初始化線程池的時候指定如果線程池滿,則新提交的任務轉為串行執行,那我們之前的寫法就會有問題了,串行執行的時候調用ContextHolder.remove();會將主線程的上下文也清理,即使后面線程池繼續并行工作,傳給子線程的上下文也已經是null了,而且這樣的問題很難在預發測試的時候發現。

    并行流中線程上下文丟失

    如果ThreadLocal碰到并行流,也會有很多有意思的事情發生,比如有下面的代碼:

    class?ParallelProcessor<T>?{public?void?process(List<T>?dataList)?{//?先校驗參數,篇幅限制先省略不寫dataList.parallelStream().forEach(entry?->?{doIt();});}private?void?doIt()?{String?session?=?ContextHolder.get();//?do?something} }

    這段代碼很容易在線下測試的過程中發現不能按照預期工作,因為并行流底層的實現也是一個ForkJoin線程池,既然是線程池,那ContextHolder.get()可能取出來的就是一個null。我們順著這個思路把代碼再改一下:

    class?ParallelProcessor<T>?{private?String?session;public?ParallelProcessor(String?session)?{this.session?=?session;}public?void?process(List<T>?dataList)?{//?先校驗參數,篇幅限制先省略不寫dataList.parallelStream().forEach(entry?->?{try?{ContextHolder.set(session);//?業務處理doIt();}?catch?(Exception?e)?{//?log?it}?finally?{ContextHolder.remove();}});}private?void?doIt()?{String?session?=?ContextHolder.get();//?do?something} }

    修改完后的這段代碼可以工作嗎?如果運氣好,你會發現這樣改又有問題,運氣不好,這段代碼在線下運行良好,這段代碼就順利上線了。不久你就會發現系統中會有一些其他很詭異的bug。原因在于并行流的設計比較特殊,父線程也有可能參與到并行流線程池的調度,那如果上面的process方法被父線程執行,那么父線程的上下文會被清理。導致后續拷貝到子線程的上下文都為null,同樣產生丟失上下文的問題。

    往期推薦

    額!Java中用戶線程和守護線程區別這么大?


    線程的故事:我的3位母親成就了優秀的我!


    Semaphore自白:限流器用我就對了!


    CyclicBarrier:人齊了,老司機就發車了!

    總結

    以上是生活随笔為你收集整理的ThreadLocal中的3个大坑,内存泄露都是小儿科!的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。