溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何使用ThreadLocal

發(fā)布時間:2021-10-11 21:19:25 來源:億速云 閱讀:139 作者:iii 欄目:開發(fā)技術

本篇內(nèi)容主要講解“如何使用ThreadLocal”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“如何使用ThreadLocal”吧!

目錄
  • 使用

  • 構造方法

    • 靜態(tài)方法

    • 公共方法

  • 內(nèi)存泄露

    • 解決方法

      • 為什么要將ThreadLocal 定義成 static 變量

    • 對ThreadLocal內(nèi)存泄漏引起的思考

      • 概述

        • 使用場景樣例代碼

        • ThreadLocal使用源碼

        • 思考問題

      • ThreadLocal解讀

      ThreadLocal 看名字 就可以看出一點頭緒來,線程本地。

      來看一下java對他的描述:

      該類提供線程本地變量。這些變量與它們的正常對應變量的不同之處在于,每個線程(通過ThreadLocal的 get 或 set方法)訪問自己的、獨立初始化的變量副本。 ThreadLocal實例通常是類中的私有靜態(tài)字段。

      上面這段話呢,一個重點就是 每個線程都有自己的專屬變量,這個專屬變量呢,是不會被其他線程影響的。

      使用

      public class ThreadLocalTwo {
          //靜態(tài)的 延長生命周期。final  不可改變
          private static final ThreadLocal<Integer> threalLocal = ThreadLocal.withInitial(() -> {
              return 0;
          });
          public static void main(String[] args) {
              new Thread(() -> {
                  while (true) {
                      //取出來
                      int inner = threalLocal.get();
                      //使用
                      System.out.println(Thread.currentThread().getName() + "   " + inner);
                      LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
                      //更新值存入
                      threalLocal.set(++inner);
                  }
              }, "three").start();
              new Thread(() -> {
                  while (true) {
                      //取出來
                      int inner = threalLocal.get();
                      //使用
                      System.out.println(Thread.currentThread().getName() + "   " + inner);
                      LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
                      //更新值存入
                      threalLocal.set(++inner);
                  }
              }, "four").start();
          }
      }

      使用這個我只是隨便寫一個demo,具體的邏輯有很多種,只要你想,就會有很多種寫法。具體看業(yè)務需求。

      個人理解

      ThreadLocal 類似于一個工具,通過這個工具,來為當前線程設定修改移除本地副本。,如果 你查看Thread的源碼會發(fā)現(xiàn)下面這段代碼

      /* ThreadLocal values pertaining to this thread. This map is maintained
           by the ThreadLocal class. */
          ThreadLocal.ThreadLocalMap threadLocals = null;

      這是靜態(tài)內(nèi)部類構造的一個字段,那么我們看一下 ThreadLocal.ThreadLocalMap的源碼.

       static class Entry extends WeakReference<ThreadLocal<?>> {
                  /** The value associated with this ThreadLocal. */
                  Object value;
                  Entry(ThreadLocal<?> k, Object v) {
                      super(k);
                      value = v;
                  }
              }
              
              /**
               * The table, resized as necessary.
               * table.length MUST always be a power of two.
               */
              private Entry[] table;

      上面代碼我們可以發(fā)現(xiàn) ThreadLocal.ThreadLocalMap這個內(nèi)部靜態(tài)類,里面還包含這一個內(nèi)部靜態(tài)類Entry。

      這個Entry 繼承了WeakReference,并且將ThreadLocal作為弱引用類型。這表明 ThreadLocal如果沒有其他的強引用時候,說不定 有可能不知道啥時候就被回收了。

      那么至于 value呢? 我可以肯定的告訴你 value不會被回收,即便 傳進來的v是個匿名類。

      value持有著線程的本地副本的引用

      Entry[] table 這個持有 entry的引用

      現(xiàn)在 ,只需要知道

      1 弱引用對象,會持有引用對象的引用,弱引用對象并不能決定 引用對象是否回收。

      2 弱引用的子類的 如果有自己的字段的話, 那么那個字段是強引用,不會被回收

      3 弱引用對象,如果是new出來的,那么弱引用對象本身也是一個強引用。弱引用對象自己不會被回收。

      構造方法

      一個默認的無參構造方法 ,沒啥好講的,,

      public ThreadLocal() {
          }

      使用

        private static final ThreadLocal<String> construct  = new ThreadLocal<>(){
              //如果 不重寫這個方法的話,默認返回null
              @Override
              protected String initialValue() {
                  return "默認值";
              }
          };

      靜態(tài)方法

      note Java8新增的方法

       public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
              return new SuppliedThreadLocal<>(supplier);
          }

      上面的這個靜態(tài)方法呢,生成一個ThreadLocal對象,參數(shù)是一個Supplier函數(shù)接口。

      下面展示一個代碼

      private static final ThreadLocal<String> local = ThreadLocal.withInitial(() -> "默認值");

      上面這段代碼使用了Lambda表達式, 比起上面 new 并且重寫方法的寫法,代碼會少很多,顯得很有逼格對不。

      如果你對java8的Lambda不清楚的話,可以看這篇文章:java Lambda表達式的使用

      公共方法

      //返回當前線程本地副本的值。如果本地副本為null,則返回初始化為調(diào)用{@link #initialValue}方法返回的值。
      public T get()
      //將當前線程的本地副本 設為 value
      public void set(T value)
      //將當前線程的本地副本移除,如果后面調(diào)用get()方法的話,會返回T initialValue()的值
      public void remove()

      內(nèi)存泄露

      接下來講一下,ThreadLocal配合線程池時候 會出現(xiàn)內(nèi)存泄漏的原理。按照我的個人理解 ,是因為內(nèi)存溢出造成的。內(nèi)存泄露指的是 原本應該回收的對象,現(xiàn)在由于種種原因,無法被回收。

      為什么上面會強調(diào) 配合線程池的時候,因為單獨線程的時候,當線程任務運行完以后,線程資源會被回收,自然 本地副本也被回收了。而線程池里面的線程不全被回收(有的不會被回收,也有的會被回收)。

      現(xiàn)在來看一下上面的Entry這個最終存儲本地副本的靜態(tài)內(nèi)部類,

         static class Entry extends WeakReference<ThreadLocal<?>> {
                  /** The value associated with this ThreadLocal. */
                  Object value;
                  Entry(ThreadLocal<?> k, Object v) {
                      super(k);
                      value = v;
                  }
              }

      下面內(nèi)容需要你對 java 內(nèi)存管理關系了解,否則 你肯定會一臉蒙蔽。

      如果 你不會 可以看我這篇文章java內(nèi)存管理關系及內(nèi)存泄露的原理

      由于它是WeakReference的子類,所以 作為引用對象的 ThreadLocal,就有可能會被Entry清除引用。如果這時候 ThreadLocal沒有其他的引用,那么它肯定就會被GC回收了。

      但是value 是強引用,而Entry 又被Entry[]持有,Entry[]又被ThreadLocalMap持有,ThreadLocalMap又被線程持有。只要線程不死或者 你不調(diào)用set,remove這兩個方法之中任何一個,那么value指向的這個對象就始終 不會被回收。因為 不符合GC回收的兩個條件的任何一個。

      試想一下如果線程池里面的線程足夠的多,并且 你傳給線程的本地副本內(nèi)存占用又很大。毫無疑問 會內(nèi)存溢出。

      解決方法

      只要調(diào)用remove 這個方法會擦出 上一個value的引用,這樣線程就不會持有上一個value指向對象的引用。就不會有內(nèi)存露出了。

      有讀者會有疑問了,上面不是說兩個放過會使value對象可以回收么,怎么上面沒有set方法呢?

      這個是因為,set方法確實可以是value指向的對象 這個引用斷開,但同時它又強引用了一個內(nèi)存空間給value。即使上一個對象被回收了,但是新對象也產(chǎn)生了。

      至于 get方法,只有在ThreadLocalMap 被GC后,調(diào)用get方法 才會將value對應的引用切斷。

      首先,我們看get源碼

        public T get() {
              Thread t = Thread.currentThread();//當前線程的引用
              //得到當前線程的ThreadLocalMap,如果沒有返回null
              ThreadLocalMap map = getMap(t);
              //存在時候走這個
              if (map != null) {
                   //與鍵關聯(lián)的項,如果沒有鍵則為null  
                   //如果ThreadLocalMap的entry 清除了ThreadLocal 對象的引用,那么這個會清除對應的value 引用
                  ThreadLocalMap.Entry e = map.getEntry(this);
                  if (e != null) {
                      @SuppressWarnings("unchecked")
                      T result = (T)e.value;
                      return result;
                  }
              }
              //當前線程 沒有設置ThreadLocalMap,那么返回initialValue()的值
              return setInitialValue();
          }

      上面這段代碼,調(diào)用了getEntry,這個方法內(nèi)部調(diào)用了 另一個方法,實現(xiàn)了當ThreadLocal被清除引用后,也清除對應的value引用,

      private Entry getEntry(ThreadLocal<?> key) {
                  //得到位置  table數(shù)組 的容量是16
                  int i = key.threadLocalHashCode & (table.length - 1);
                  Entry e = table[i];
                   //key沒有被回收后
                  if (e != null && e.get() == key)
                      return e;
                  else
                       //這個key被回收 調(diào)用,將對應的value 釋放引用
                      return getEntryAfterMiss(key, i, e);
              }

      我們看見最后調(diào)用 getEntryAfterMiss(key, i, e),這個方法 也不是最終的擦除value引用的方法,我們接著往下看

       private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
                  Entry[] tab = table;
                  int len = tab.length;
                  while (e != null) {
                       //得到弱引用對象 持有的引用對象的引用
                      ThreadLocal<?> k = e.get();
                      //ThreadLocal沒有被回收
                      if (k == key)
                          return e;
                      
                      if (k == null)
                          //entry 清除ThreadLocal的引用 
                         //通過entry[]數(shù)組的元素entry 清除entry的value引用
                          expungeStaleEntry(i);
                      else
                          i = nextIndex(i, len);
                      e = tab[i];
                  }
                  return null;
              }

      這上面呢,我們要關注expungeStaleEntry(i),這個才是最終的擦除entry的value對象的引用。 看一下 expungeStaleEntry(i)的源碼

       private int expungeStaleEntry(int staleSlot) {
                  Entry[] tab = table;//得到table引用
                  int len = tab.length;//得到table的長度,不出意外 應該是16
                  // expunge entry at staleSlot
                 //下面兩句代碼 是關鍵。
                  tab[staleSlot].value = null;
                  tab[staleSlot] = null;
                  size--;
                  // Rehash until we encounter null
                  Entry e;
                  int i;
                  for (i = nextIndex(staleSlot, len);
                       (e = tab[i]) != null;
                       i = nextIndex(i, len)) {
                      ThreadLocal<?> k = e.get();
                      if (k == null) {
                          e.value = null;
                          tab[i] = null;
                          size--;
                      } else {
                          int h = k.threadLocalHashCode & (len - 1);
                          if (h != i) {
                              tab[i] = null;
                              // Unlike Knuth 6.4 Algorithm R, we must scan until
                              // null because multiple entries could have been stale.
                              while (tab[h] != null)
                                  h = nextIndex(h, len);
                              tab[h] = e;
                          }
                      }
                  }
                  return i;
              }

      上面這段代碼很長,我們不必細看個,關注下面這兩行代碼就行

      tab[staleSlot].value = null;//清除引用  這樣 GC就可以回收了
                  tab[staleSlot] = null;//清除自身的引用

      通過entry[staleSlot]得到存儲的entry ,通過entry清除entry的value引用。

      這樣大家明白了吧,get也是可以起到和remove一樣的效果的。

      我們再看一下remove的源碼

       public void remove() {
               ThreadLocalMap m = getMap(Thread.currentThread());
               if (m != null)
                   m.remove(this);
           }

      上面這段代碼沒什么說的,直接看ThreadLocalMap的remove方法

      private void remove(ThreadLocal<?> key) {
                  Entry[] tab = table;
                  int len = tab.length;
                  //得到位置,因為存的時候 也是按照這個規(guī)則來的,
                  int i = key.threadLocalHashCode & (len-1);
                  for (Entry e = tab[i];
                       e != null;
                       e = tab[i = nextIndex(i, len)]) {
                       //這里有可能會發(fā)生 ThreadLocal 被entry清除引用,那么value就被線程引用了,如果不調(diào)用set,get方法的話,只能等待線程銷毀。
                      if (e.get() == key) {
                          //調(diào)用弱引用的方法 , 將引用對象的引用清除
                          e.clear();
                          //擦出ThreadLocal 對應的value
                          expungeStaleEntry(i);
                          return;
                      }
                  }
              }

      上面調(diào)用了 expungeStaleEntry 擦除。

      set

      我們關注這個方法

        private void expungeStaleEntries() {
                  Entry[] tab = table;
                  int len = tab.length;
                  for (int j = 0; j < len; j++) {
                      Entry e = tab[j];
                      if (e != null && e.get() == null)
                          //擦除
                          expungeStaleEntry(j);
                  }
              }

      這個呢 循環(huán)調(diào)用了expungeStaleEntry(j)方法 ,也是擦除了value的對象引用。

      為什么要將ThreadLocal 定義成 static 變量

      延長生命周期,之所以是static 是因為,ThreadLocal 我們更應該將他看成是 工具。

      對ThreadLocal內(nèi)存泄漏引起的思考

      概述

      最近在對一個項目進行重構,用到了ThreadLocal。

      場景如下:

      外圍系統(tǒng)會調(diào)用接口上傳數(shù)據(jù),在接口中要記錄數(shù)據(jù)的變化Id,在上傳數(shù)據(jù)完后需要集中在一個地方把這些Id以消息形式發(fā)送出去。

      使用場景樣例代碼
      public Result<Void> uploadOrder(TotalPayInfoVo totalPayInfoVo) {
              try {
                  saveTotalPayInfoVo(totalPayInfoVo);
                  //發(fā)送消息
                  UnitWork.getCurrent().pushMessage();
              } catch (Exception e) {
                  cashLogger.error("uploadOrder error,data: {}, error: {}", JSON.toJSONString(totalPayInfoVo), e);
                  throw new RuntimeException("保存失敗", e);
              } finally {
                  UnitWork.clean();//
              }
              return ResultUtil.successResult();避免內(nèi)存泄漏
          }
      ThreadLocal使用源碼
      /**
       * 工作單元,在同一個線程中負責記錄一個事件或者一個方法或者一個事務過程中產(chǎn)生的變化,等操作結束后再處理這種變化。
       */
      public class UnitWork {
          private UnitWork() {
          }
          private static ThreadLocal<UnitWork> current = new ThreadLocal<UnitWork>() {
              protected UnitWork initialValue() {
                  return new UnitWork();
              }
          };
          /**
           * 狀態(tài)變化的instance
           */
          private Set<String> statusChangedInstances = new HashSet<>();
          public void addStatusChangedInstance(String instance) {
              statusChangedInstances.add(instance);
          }
          /**
           * 推送消息
           */
          public void pushMessage() {
             for(String id : statusChangedInstances){
                  //異步發(fā)消息
             }
          }
          public static UnitWork getCurrent() {
              return current.get();
          }
          /**
           * 刪除當前線程的工作單元,建議放在finally中調(diào)用,避免內(nèi)存泄漏
           */
          public static void clean() {
              current.remove();
          }
      }
      思考問題

      為了避免內(nèi)存泄漏,每次用完做一下clean清理操作。發(fā)送消息的過程是異步的,意味著clean的時候可能和發(fā)送消息同時進行。那么會不會把這些Id清理掉?那么可能造成消息發(fā)送少了。要回答這個問題,首先要搞懂ThreadLocal的引用關系,remove操作做了什么?

      ThreadLocal解讀

      ThreadLocal可以分別在各個線程保存變量獨立副本。每個線程都有ThreadLocalMap,顧名思義,類似Map容器,不過是用數(shù)組Entry[]來模擬的。那么既然類似Map,肯定會存在Key。其實Key是ThreadLocal類型,Key的值是ThreadLocal的HashCode,即通過threadLocalHashCode計算出來的值。

      這個Map的Entry并不是ThreadLocal,而是一個帶有弱引用的Entry。既然是弱引用,每次GC的時候都會回收。

      static class Entry extends WeakReference<ThreadLocal> {
                  /** The value associated with this ThreadLocal. */
                  Object value;
                  Entry(ThreadLocal k, Object v) {
                      super(k);
                      value = v;
                  }
              }

      而Key對應的value就是要保存在線程副本Object,這里指的就是UnitWork的實例。調(diào)用ThreadLocal的get方法時,首先找到當前線程的ThreadLocalMap,然后根據(jù)這個ThreadLocal算出來的hashCode找到保存線程副本Object。

      他們的關系對應如下:

      如何使用ThreadLocal

      ThreadLocal在remove的時候,會調(diào)用Entry的clear,即弱引用的clear方法。把Key->ThreadLocal的引用去掉。接下來的expungeStaleEntry會把entry中value引用設置為null。

      /**
               * Remove the entry for key.
               */
              private void remove(ThreadLocal key) {
                  Entry[] tab = table;
                  int len = tab.length;
                  int i = key.threadLocalHashCode & (len-1);
                  for (Entry e = tab[i];
                       e != null;
                       e = tab[i = nextIndex(i, len)]) {
                      if (e.get() == key) {
                          e.clear();
                          expungeStaleEntry(i);
                          return;
                      }
                  }
              }

      現(xiàn)在可以回答之前提前的問題。雖然ThreadLocal和當前線程都會與Object脫離了引用的關系,但是最重要一點就是異步的線程仍然存在一條強引用路徑到Object,即到UnitWork實例的強引用。因此GC然后不會回收UnitWork的實例,發(fā)消息還是不會少發(fā)或者出現(xiàn)空指針情況。

      到此,相信大家對“如何使用ThreadLocal”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關內(nèi)容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!

      向AI問一下細節(jié)

      免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

      AI