<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

          共 11082字,需瀏覽 23分鐘

           ·

          2021-06-28 08:30

          本文講解ThreadLocal、InheritableThreadLocal與TransmittableThreadLocal。

          有關(guān)本文的實驗代碼,可以查看文末補充:“比較一下ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在線程池復(fù)用線程的情況下的執(zhí)行情況”。

          ThreadLocal

          ThreadLocal的使用場景

          1. 分布式跟蹤系統(tǒng)

          2. 日志收集記錄系統(tǒng)上下文

          3. Session級Cache

          4. 應(yīng)用容器或上層框架跨應(yīng)用代碼給下層SDK傳遞信息

          舉例:

          1. Spring的事務(wù)管理,用ThreadLocal存儲Connection,從而各個DAO可以獲取同一Connection,可以進(jìn)行事務(wù)回滾,提交等操作。

          2. 某些業(yè)務(wù)場景下,需要強(qiáng)制讀主庫來保證數(shù)據(jù)的一致性。在Sharding-JDBC中使用了ThreadLocal來存儲相關(guān)配置信息,實現(xiàn)優(yōu)雅的數(shù)據(jù)傳遞。

          3. Spring Cloud Zuul用過濾器可以實現(xiàn)權(quán)限認(rèn)證,日志記錄,限流等功能,多個過濾器之間透傳數(shù)據(jù),底層使用了ThreadLocal。

          4. 在整個鏈路的日志中輸出當(dāng)前登錄的用戶ID,首先就得在攔截器獲取過濾器中獲取用戶。ID,然后將用戶ID進(jìn)行存儲到slf4j的MDC對象(底層使用ThreadLocal),然后進(jìn)行鏈路傳遞打印日志。

          ThreadLocal的結(jié)構(gòu)

          1. ThreadLocal的get()、set()方法,實際操作的都是Thread.currentThread(),即當(dāng)前線程的threadLocals變量。

          2. threadLocals變量包含了一個map成員變量(ThreadLocalMap)。

          3. ThreadLocalMap的key為當(dāng)前ThreadLocal, value為set的值。

          相同的key在不同的散列表中的值必然是獨立的,每個線程都是在各自的散列表中執(zhí)行操作,如下圖所示:

          ThreadLocal的set方法:

          public void set(T value) {
          //currentThread是個native方法,會返回對當(dāng)前執(zhí)行線程對象的引用。
          Thread t = Thread.currentThread();
          //getMap 返回線程自身的threadLocals
          ThreadLocalMap map = getMap(t);
          if (map != null) {
          //value set到線程自身的ThreadLocalMap中了
          map.set(this, value);
          } else {
          //線程自身的ThreadLocalMap未初始化,則先初始化,再set
          createMap(t, value);
          }
          }
          ThreadLocalMap getMap(Thread t) {
          return t.threadLocals;
          }

          ThreadLocal.ThreadLocalMap threadLocals = null;

          ThreadLocal在set的時候,沒有進(jìn)行相應(yīng)的深拷貝,所以ThreadLocal要想做線程隔離,必須是基本類型或者是Runable實現(xiàn)類的局部變量。

          ThreadLocal造成內(nèi)存泄漏

          ThreadLocalMap內(nèi)部Entry:

          static class Entry extends WeakReference<ThreadLocal<?>> {
          /** The value associated with this ThreadLocal. */
          Object value;

          Entry(ThreadLocal<?> k, Object v) {
          super(k);
          value = v;
          }
          }

          從代碼中看到,Entry繼承了WeakReference,并將ThreadLocal設(shè)置為了WeakReference,value設(shè)置為強(qiáng)引用。也就是:當(dāng)沒有強(qiáng)引用指向ThreadLocal變量時,它可被回收。

          內(nèi)存泄漏風(fēng)險:ThreadLocalMap維護(hù)ThreadLocal變量與具體實例的映射,當(dāng)ThreadLocal變量被回收后(變?yōu)閚ull),無法路由到ThreadLocalMap。而該Entry還是在ThreadLocalMap中,從而這些無法清理的Entry,會造成內(nèi)存泄漏。

          所以,在使用ThreadLocal的時候,會話結(jié)束前務(wù)必使用ThreadLocal.remove方法(remove方法會將Entry的value及Entry自身設(shè)置為null并進(jìn)行清理)。

          ThreadLocal的最佳實踐

          1. ThreadLocal使用時必須顯式地調(diào)用remove方法來避免內(nèi)存泄漏。

          2. ThreadLocal對象建議使用static修飾。這樣做的好處是可以避免重復(fù)創(chuàng)建對象所導(dǎo)致的浪費(類第一次被使用時裝載,只分配一塊存儲空間)。壞處是正好形成內(nèi)存泄漏所需的條件(延長了ThreadLocal的生命周期,因此需要remove方法兜底)。

          3. 注釋說明使用場景。

          4. 對性能有極致要求可以參考開源框架優(yōu)化后的類,比如Netty的FastThreadLocal、Dubbo的InternalThreadLocal等。

          InheritableThreadLocal

          在全鏈路跟蹤框架中,Trace信息的傳遞功能是基于ThreadLocal的。但實際業(yè)務(wù)中可能會使用異步調(diào)用,這樣就會丟失Trace信息,破壞了鏈路的完整性。

          此時可以使用JDK實現(xiàn)的InheritableThreadLocal,但它只支持父子線程間傳遞信息(例如:paramstream、new Thread等)。

          Thread內(nèi)部為InheritableThreadLocal開辟了一個單獨的ThreadLocalMap(與ThreadLocal并列的成員變量)。在父線程創(chuàng)建一個子線程的時候,會檢查這個ThreadLocalMap是否為空,不為空則會淺拷貝給子線程的ThreadLocalMap。

          從類的繼承層次來看,InheritableThreadLocal只是在ThreadLocal的get、set、remove流程中,重寫了getMap、createMap方法,整體流程與ThreadLocal保持一致。

          Thread的init相關(guān)邏輯如下:

          if (parent.inheritableThreadLocals != null)
          this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

          需要注意的是拷貝為淺拷貝。

          TransmittableThreadLocal

          InheritableThreadLocal可以在父線程創(chuàng)建子線程的時候?qū)hreadLocal中的值傳遞給子線程,從而完成鏈路跟蹤框架中的上下文傳遞。

          但大部分業(yè)務(wù)應(yīng)用都會使用線程池,這種復(fù)用線程的池化場景中,線程池中的線程和主線程并不都是父子線程的關(guān)系,不能直接使用InheritableThreadLocal。

          例如從Tomcat的線程(池化)提交task到業(yè)務(wù)線程池,就不能直接使用InheritableThreadLocal。

          Transmittable ThreadLocal(簡稱TTL)是阿里開源的庫,繼承了InheritableThreadLocal,實現(xiàn)線程本地變量在線程池的執(zhí)行過程中,能正常的訪問父線程設(shè)置的線程變量。

          TransmittableThreadLocal實現(xiàn)原理

          InheritableThreadLocal不支持池化線程提交task到業(yè)務(wù)線程池的根本原因是,父線程創(chuàng)建子線程時,子線程InheritableThreadLocal只會復(fù)制一次環(huán)境變量。要支持線程池中能訪問提交任務(wù)線程的本地變量,只需要在線程向線程池提交任務(wù)時復(fù)制父線程的上下環(huán)境,那在線程池中就能夠訪問到父線程中的本地變量,實現(xiàn)本地環(huán)境變量在線程池調(diào)用中的透傳。

          源碼見于參考文檔1,README有很詳細(xì)的講解,核心源碼也不難,建議看看。

          此外,項目引入TTL的時候,可以使用Java Agent植入修飾代碼,修改runnable或者callable類,可以做到對應(yīng)用代碼無侵入(這個在README也有相關(guān)講解)。

          補充說明

          ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在線程池復(fù)用線程的情況下的執(zhí)行情況如下:

          1.線程局部變量為基礎(chǔ)類型

          1.1 ThreadLocal

          class TransmittableThreadLocalTest1 {
          static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
          static ExecutorService executorService =
          Executors.newFixedThreadPool(1);

          public static void main(String[] args) throws InterruptedException {
          System.out.println("主線程開啟");
          threadLocal.set(1);
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);

          threadLocal.set(2);
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          //[沒有讀到了主線程修改后的新值]
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          threadLocal.set(3);
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);
          //依舊讀取的是 2
          System.out.println("主線程讀取本地變量:" + threadLocal.get());
          }
          }

          輸出結(jié)果為:

          主線程開啟
          主線程讀取本地變量:1
          子線程讀取本地變量:null
          主線程讀取本地變量:2
          子線程讀取本地變量:null
          子線程讀取本地變量:3
          主線程讀取本地變量:2

          1.2 InheritableThreadLocal

          class TransmittableThreadLocalTest2 {
          static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
          static ExecutorService executorService =
          Executors.newFixedThreadPool(1);

          public static void main(String[] args) throws InterruptedException {
          System.out.println("主線程開啟");
          threadLocal.set(1);
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);

          threadLocal.set(2);
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          //[沒有讀到了主線程修改后的新值]
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          threadLocal.set(3);
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);
          //依舊讀取的是 2
          System.out.println("主線程讀取本地變量:" + threadLocal.get());
          }
          }

          輸出結(jié)果為:

          主線程開啟
          主線程讀取本地變量:1
          子線程讀取本地變量:1
          主線程讀取本地變量:2
          子線程讀取本地變量:1
          子線程讀取本地變量:3
          主線程讀取本地變量:2

          1.3 TransmittableThreadLocal

          class TransmittableThreadLocalTest3 {
          static ThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
          static ExecutorService executorService =
          TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

          public static void main(String[] args) throws InterruptedException {
          System.out.println("主線程開啟");
          threadLocal.set(1);
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);

          threadLocal.set(2);
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          //[讀到了主線程修改后的新值]
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          threadLocal.set(3);
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);
          //依舊讀取的是 2
          System.out.println("主線程讀取本地變量:" + threadLocal.get());
          }
          }

          輸出結(jié)果為:

          主線程開啟
          主線程讀取本地變量:1
          子線程讀取本地變量:1
          主線程讀取本地變量:2
          子線程讀取本地變量:2
          子線程讀取本地變量:3
          主線程讀取本地變量:2

          2.線程局部變量為類對象

          首先定義一個數(shù)據(jù)類:

          @Data
          @AllArgsConstructor
          class UserSession{
          String uuid;
          String nickname;
          }

          2.1 ThreadLocal

          class TransmittableThreadLocalTest4 {
          static ThreadLocal<UserSession> threadLocal = new ThreadLocal<>();
          static ExecutorService executorService =
          Executors.newFixedThreadPool(1);

          public static void main(String[] args) throws InterruptedException {
          System.out.println("主線程開啟");
          threadLocal.set(new UserSession("001","hello"));
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);

          threadLocal.get().setNickname("world");
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          //[沒有讀到了主線程修改后的新值]
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          threadLocal.get().setNickname("Java");
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);
          //依舊讀取的是 world
          System.out.println("主線程讀取本地變量:" + threadLocal.get());
          }
          }

          輸出結(jié)果為:

          主線程開啟
          主線程讀取本地變量:UserSession(uuid=001, nickname=hello)
          子線程讀取本地變量:null
          主線程讀取本地變量:UserSession(uuid=001, nickname=world)
          子線程讀取本地變量:null
          主線程讀取本地變量:UserSession(uuid=001, nickname=world)

          2.2 InheritableThreadLocal

          class TransmittableThreadLocalTest5 {
          static ThreadLocal<UserSession> threadLocal = new InheritableThreadLocal<>();
          static ExecutorService executorService =
          Executors.newFixedThreadPool(1);

          public static void main(String[] args) throws InterruptedException {
          System.out.println("主線程開啟");
          threadLocal.set(new UserSession("001","hello"));
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);

          threadLocal.get().setNickname("world");
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          //[讀到了主線程修改后的新值]
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          threadLocal.get().setNickname("Java");
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);
          //讀取的是 Java(因為淺拷貝)
          System.out.println("主線程讀取本地變量:" + threadLocal.get());
          }
          }
          主線程開啟
          主線程讀取本地變量:UserSession(uuid=001, nickname=hello)
          子線程讀取本地變量:UserSession(uuid=001, nickname=hello)
          主線程讀取本地變量:UserSession(uuid=001, nickname=world)
          子線程讀取本地變量:UserSession(uuid=001, nickname=world)
          子線程讀取本地變量:UserSession(uuid=001, nickname=Java)
          主線程讀取本地變量:UserSession(uuid=001, nickname=Java)

          2.3 InheritableThreadLocal

          class TransmittableThreadLocalTest6 {
          static ThreadLocal<UserSession> threadLocal = new TransmittableThreadLocal<>();
          static ExecutorService executorService =
          TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

          public static void main(String[] args) throws InterruptedException {
          System.out.println("主線程開啟");
          threadLocal.set(new UserSession("001","hello"));
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);

          threadLocal.get().setNickname("world");
          System.out.println("主線程讀取本地變量:" + threadLocal.get());

          executorService.submit(() -> {
          //[讀到了主線程修改后的新值]
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          threadLocal.get().setNickname("Java");
          System.out.println("子線程讀取本地變量:" + threadLocal.get());
          });

          TimeUnit.SECONDS.sleep(1);
          //讀取的是 Java(因為淺拷貝)
          System.out.println("主線程讀取本地變量:" + threadLocal.get());
          }
          }

          輸出結(jié)果與上面2.2的結(jié)果一樣

          參考文檔:

          1. https://github.com/alibaba/transmittable-thread-local


          瀏覽 41
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  婷婷午夜精品久久久久久性色AV | 久久久六月 | 青娱乐青青草论坛在线 | 5252色成人免费 | 你懂的网址在线 |