<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ThreadLocal巨坑!內(nèi)存泄露只是小兒科...

          共 9597字,需瀏覽 20分鐘

           ·

          2022-06-27 20:49

          Hollis的新書限時折扣中,一本深入講解Java基礎(chǔ)的干貨筆記!

          我在參加 Code Review 的時候不止一次聽到有同學(xué)說:我寫的這個上下文工具沒問題,在線上跑了好久了。其實(shí)這種想法是有問題的,ThreadLocal 寫錯難,但是用錯就很容易。


          本文將會詳細(xì)總結(jié) ThreadLocal 容易用錯的三個坑:

          • 內(nèi)存泄露

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

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


          內(nèi)存泄露


          由于 ThreadLocal 的 key 是弱引用,因此如果使用后不調(diào)用 remove 清理的話會導(dǎo)致對應(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 引用,無法被 GC,但是其 key 對 ThreadLocal 實(shí)例的引用是一個弱引用。


          本來 ThreadLocal 的實(shí)例被 localCache 和 ThreadLocalMap 的 key 同時引用,但是當(dāng) localCache 的引用被重置之后,則 ThreadLocal 的實(shí)例只有 ThreadLocalMap 的 key 這樣一個弱引用了,此時這個實(shí)例在 GC 的時候能夠被清理。

          其實(shí)看過 ThreadLocal 源碼的同學(xué)會知道,ThreadLocal 本身對于 key 為 null 的 Entity 有自清理的過程,但是這個過程是依賴于后續(xù)對 ThreadLocal 的繼續(xù)使用。


          假如上面的這段代碼是處于一個秒殺場景下,會有一個瞬間的流量峰值,這個流量峰值也會將集群的內(nèi)存打到高位(或者運(yùn)氣不好的話直接將集群內(nèi)存打滿導(dǎo)致故障)。


          后面由于峰值流量已過,對 ThreadLocal 的調(diào)用也下降,會使得 ThreadLocal 的自清理能力下降,造成內(nèi)存泄露。


          ThreadLocal 的自清理是錦上添花,千萬不要指望他雪中送碳。


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


          Tomcat 官網(wǎng)對在 web 容器中使用 ThreadLocal 引起的內(nèi)存泄露做了一個總結(jié),詳見:
          https://cwiki.apache.org/confluence/display/tomcat/MemoryLeakProtection


          這里我們列舉其中的一個例子,熟悉 Tomcat 的同學(xué)知道,Tomcat 中的 web 應(yīng)用由 Webapp Classloader 這個類加載器的。


          并且 Webapp Classloader 是破壞雙親委派機(jī)制實(shí)現(xiàn)的,即所有的 web 應(yīng)用先由 Webapp classloader 加載,這樣的好處就是可以讓同一個容器中的 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();
           }
          }


          需要注意這個例子中的兩個非常關(guān)鍵的點(diǎn):

          • MyCounter以及MyThreadLocal必須放到web應(yīng)用的路徑中,保被 Webapp Classloader 加載。
          • ThreadLocal 類一定得是 ThreadLocal 的繼承類,比如例子中的 MyThreadLocal,因?yàn)?ThreadLocal 本來被 Common Classloader 加載,其生命周期與 Tomcat 容器一致。ThreadLocal 的繼承類包括比較常見的 NamedThreadLocal,注意不要踩坑。


          假如 LeakingServlet 所在的 Web 應(yīng)用啟動,MyThreadLocal 類也會被 Webapp Classloader 加載。


          如果此時 web 應(yīng)用下線,而線程的生命周期未結(jié)束(比如為LeakingServlet 提供服務(wù)的線程是一個線程池中的線程)。


          那會導(dǎo)致 myThreadLocal 的實(shí)例仍然被這個線程引用,而不能被 GC,期初看來這個帶來的問題也不大,因?yàn)?myThreadLocal 所引用的對象占用的內(nèi)存空間不太多。


          問題在于 myThreadLocal 間接持有加載 web 應(yīng)用的 webapp classloader 的引用(通過 myThreadLocal.getClass().getClassLoader() 可以引用到)。


          而加載 web 應(yīng)用的 webapp classloader 有持有它加載的所有類的引用,這就引起了 Classloader 泄露,它泄露的內(nèi)存就非??捎^了。


          線程池中線程上下文丟失


          ThreadLocal 不能在父子線程中傳遞,因此最常見的做法是把父線程中的 ThreadLocal 值拷貝到子線程中。


          因此大家會經(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ù)定義長這樣:
          class BizTask<Timplements Callable<T>  {
              private String session = null;

              public BizTask(String session) {
                  this.session = session;
              }

              @Override
              public T call(){
                  try {
                      ContextHolder.set(this.session);
                      // 執(zhí)行業(yè)務(wù)邏輯
                  } catch(Exception e){
                      //log error
                  } finally {
                      ContextHolder.remove(); // 清理 ThreadLocal 的上下文,避免線程復(fù)用時context互串
                  }
                  return null;
              }
          }


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

          }


          這么寫倒也沒有問題,我們再看看線程池的設(shè)置:
          ThreadPoolExecutor executorPool = new ThreadPoolExecutor(204030, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(40), new XXXThreadFactory(), ThreadPoolExecutor.CallerRunsPolicy);


          其中最后一個參數(shù)控制著當(dāng)線程池滿時,該如何處理提交的任務(wù),內(nèi)置有 4 種策略:
          ThreadPoolExecutor.AbortPolicy //直接拋出異常
          ThreadPoolExecutor.DiscardPolicy //丟棄當(dāng)前任務(wù)
          ThreadPoolExecutor.DiscardOldestPolicy //丟棄工作隊(duì)列頭部的任務(wù)
          ThreadPoolExecutor.CallerRunsPolicy //轉(zhuǎn)串行執(zhí)行


          可以看到,我們初始化線程池的時候指定如果線程池滿,則新提交的任務(wù)轉(zhuǎn)為串行執(zhí)行。


          那我們之前的寫法就會有問題了,串行執(zhí)行的時候調(diào)用 ContextHolder.remove(); 會將主線程的上下文也清理,即使后面線程池繼續(xù)并行工作,傳給子線程的上下文也已經(jīng)是 null 了,而且這樣的問題很難在預(yù)發(fā)測試的時候發(fā)現(xiàn)。


          并行流中線程上下文丟失


          如果 ThreadLocal 碰到并行流,也會有很多有意思的事情發(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
              }
          }


          這段代碼很容易在線下測試的過程中發(fā)現(xiàn)不能按照預(yù)期工作,因?yàn)椴⑿辛鞯讓拥膶?shí)現(xiàn)也是一個 ForkJoin 線程池,既然是線程池,那 ContextHolder.get() 可能取出來的就是一個 null。


          我們順著這個思路把代碼再改一下:
          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)氣好,你會發(fā)現(xiàn)這樣改又有問題,運(yùn)氣不好,這段代碼在線下運(yùn)行良好,這段代碼就順利上線了。不久你就會發(fā)現(xiàn)系統(tǒng)中會有一些其他很詭異的 bug。


          原因在于并行流的設(shè)計(jì)比較特殊,父線程也有可能參與到并行流線程池的調(diào)度,那如果上面的 process 方法被父線程執(zhí)行,那么父線程的上下文會被清理。導(dǎo)致后續(xù)拷貝到子線程的上下文都為 null,同樣產(chǎn)生丟失上下文的問題。


          我的新書《深入理解Java核心技術(shù)》已經(jīng)上市了,上市后一直蟬聯(lián)京東暢銷榜中,目前正在6折優(yōu)惠中,想要入手的朋友千萬不要錯過哦~長按二維碼即可購買~


          長按掃碼享受6折優(yōu)惠


          往期推薦

          電商紅包雨是如何實(shí)現(xiàn)的?拿去面試用(典型高并發(fā))


          ArrayList#subList這四個坑,一不小心就中招


          去 OPPO 面試, 被問麻了。。。




          有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)

          歡迎大家關(guān)注Java之道公眾號


          好文章,我在看??

          瀏覽 45
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一级黄片a | 天堂色| 欧美成人一区免费视频 | 久久无码视频电影 | 含羞草视频一区二区三区在线无码 |