ThreadLocal父子線程傳遞實現方案


介紹InheritableThreadLocal之前,假設對 ThreadLocal 已經有了一定的理解,比如基本概念,原理,如果沒有,可以參考:ThreadLocal源碼分析解密.在講解之前我們先列舉有關ThreadLocal的幾個關鍵點

每一個Thread線程都有屬於自己的ThreadLocalMap,里面有一個弱引用的Entry(ThreadLocal,Object),如下

Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
    }
從ThreadLocal中get值的時候,首先通過Thread.currentThread得到當前線程,然后拿到這個線程的ThreadLocalMap。再傳遞當前ThreadLocal對象(結合上一點)。取得Entry中的value值

set值的時候同理,更改的是當先線程的ThreadLocalMap中Entry中key為當前Threadlocal對象的value值
Threadlocal bug?
如果子線程想要拿到父線程的中的ThreadLocal值怎么辦呢?比如會有以下的這種代碼的實現。由於ThreadLocal的實現機制,在子線程中get時,我們拿到的Thread對象是當前子線程對象,那么他的ThreadLocalMap是null的,所以我們得到的value也是null。

final ThreadLocal threadLocal=new ThreadLocal(){
            @Override
            protected Object initialValue() {
                return "xiezhaodong";
            }
        };
 new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.get();//NULL
            }
        }).start();
InheritableThreadLocal實現
那其實很多時候我們是有子線程獲得父線程ThreadLocal的需求的,要如何解決這個問題呢?這就是InheritableThreadLocal這個類所做的事情。先來看下InheritableThreadLocal所做的事情。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
   
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * 重寫Threadlocal類中的getMap方法,在原Threadlocal中是返回
     *t.theadLocals,而在這么卻是返回了inheritableThreadLocals,因為
     * Thread類中也有一個要保存父子傳遞的變量
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * 同理,在創建ThreadLocalMap的時候不是給t.threadlocal賦值
     *而是給inheritableThreadLocals變量賦值
     * 
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
以上代碼大致的意思就是,如果你使用InheritableThreadLocal,那么保存的所有東西都已經不在原來的t.thradLocals里面,而是在一個新的t.inheritableThreadLocals變量中了。下面是Thread類中兩個變量的定義

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

   /*
    * InheritableThreadLocal values pertaining to this thread. This map is
    * maintained by the InheritableThreadLocal class.
    */
   ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Q:InheritableThreadLocal是如何實現在子線程中能拿到當前父線程中的值的呢?
A:一個常見的想法就是把父線程的所有的值都copy到子線程中。
下面來看看在線程new Thread的時候線程都做了些什么?


private void init(ThreadGroup g, Runnable target, String name,
                     long stackSize, AccessControlContext acc) {
       //省略上面部分代碼
       if (parent.inheritableThreadLocals != null)
       //這句話的意思大致不就是,copy父線程parent的map,創建一個新的map賦值給當前線程的inheritableThreadLocals。
           this.inheritableThreadLocals =
               ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
      //ignore
   }
而且,在copy過程中是淺拷貝,key和value都是原來的引用地址


private ThreadLocalMap(ThreadLocalMap parentMap) {
           Entry[] parentTable = parentMap.table;
           int len = parentTable.length;
           setThreshold(len);
           table = new Entry[len];
           for (int j = 0; j < len; j++) {
               Entry e = parentTable[j];
               if (e != null) {
                   ThreadLocal key = e.get();
                   if (key != null) {
                       Object value = key.childValue(e.value);
                       Entry c = new Entry(key, value);
                       int h = key.threadLocalHashCode & (len - 1);
                       while (table[h] != null)
                           h = nextIndex(h, len);
                       table[h] = c;
                       size++;
                   }
               }
           }
       }
恩,到了這里,大致的解釋了一下InheritableThreadLocal為什么能解決父子線程傳遞Threadlcoal值的問題。

在創建InheritableThreadLocal對象的時候賦值給線程的t.inheritableThreadLocals變量
在創建新線程的時候會check父線程中t.inheritableThreadLocals變量是否為null,如果不為null則copy一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去
因為復寫了getMap(Thread)和CreateMap()方法,所以get值得時候,就可以在getMap(t)的時候就會從t.inheritableThreadLocals中拿到map對象,從而實現了可以拿到父線程ThreadLocal中的值
so,在最開始的代碼示例中,如果把ThreadLocal對象換成InheritableThreadLocal對象,那么get到的字符會是“xiezhaodong”而不是NULL

InheritableThreadLocal還有問題嗎?
問題場景
我們在使用線程的時候往往不會只是簡單的new Thrad對象,而是使用線程池,當然線程池的好處多多。這里不詳解,既然這里提出了問題,那么線程池會給InheritableThreadLocal帶來什么問題呢?我們列舉一下線程池的特點:

為了減小創建線程的開銷,線程池會緩存已經使用過的線程
生命周期統一管理,合理的分配系統資源
對於第一點,如果一個子線程已經使用過,並且會set新的值到ThreadLocal中,那么第二個task提交進來的時候還能獲得父線程中的值嗎?比如下面這種情況(雖然是線程,用sleep盡量讓他們串行的執行):

final InheritableThreadLocal<Span> inheritableThreadLocal = new InheritableThreadLocal<Span>();
      inheritableThreadLocal.set(new Span("xiexiexie"));
      //輸出 xiexiexie
      Object o = inheritableThreadLocal.get();
      Runnable runnable = new Runnable() {
          @Override
          public void run() {
              System.out.println("========");
              inheritableThreadLocal.get();
              inheritableThreadLocal.set(new Span("zhangzhangzhang");
              inheritableThreadLocal.get();
          }
      };
  
      ExecutorService executorService = Executors.newFixedThreadPool(1);
      executorService.submit(runnable);
      TimeUnit.SECONDS.sleep(1);
      executorService.submit(runnable);
      TimeUnit.SECONDS.sleep(1);
      System.out.println("========");
      Span span = inheritableThreadLocal.get();
  }
  static class Span {
      public String name;
      public int age;
      public Span(String name) {
          this.name = name;
      }
  }
輸出的會是

xiexiexie
 ========
xiexiexie
zhangzhangzhang
 ========
zhangzhangzhang
zhangzhangzhang
 ========
xiexiexie
造成這個問題的原因是什么呢,下圖大致講解一下整個過程的變化情況,如圖所示,由於B任務提交的時候使用了,A任務的緩存線程,A緩存線程的InheritableThreadLocal中的value已經被更新成了”zhangzhangzhang“。B任務在代碼內獲得值的時候,直接從t.InheritableThreadLocal中獲得值,所以就獲得了線程A中心設置的值,而不是父線程中InheritableThreadLocal的值。

so,InheritableThreadLocal還是不能夠解決線程池當中獲得父線程中ThreadLocal中的值。

造成問題的原因
那么造成這個問題的原因是什么呢?如何讓任務之間使用緩存的線程不受影響呢?實際原因是,我們的線程在執行完畢的時候並沒有清除ThreadLocal中的值,導致后面的任務重用現在的localMap。

解決方案
如果我們能夠,在使用完這個線程的時候清除所有的localMap,在submit新任務的時候在重新重父線程中copy所有的Entry。然后重新給當前線程的t.inhertableThreadLocal賦值。這樣就能夠解決在線程池中每一個新的任務都能夠獲得父線程中ThreadLocal中的值而不受其他任務的影響,因為在生命周期完成的時候會自動clear所有的數據。Alibaba的一個庫解決了這個問題github:alibaba/transmittable-thread-local

transmittable-thread-local實現原理
如何使用
這個庫最簡單的方式是這樣使用的,通過簡單的修飾,使得提交的runable擁有了上一節所述的功能。具體的API文檔詳見github,這里不再贅述

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Runnable task = new Task("1");
// 額外的處理,生成修飾了的對象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);

// Task中可以讀取, 值是"value-set-in-parent"
String value = parent.get();
原理簡述
這個方法TtlRunnable.get(task)最終會調用構造方法,返回的是該類本身,也是一個Runable,這樣就完成了簡單的裝飾。最重要的是在run方法這個地方。
public final class TtlRunnable implements Runnable { private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef; private final Runnable runnable; private final boolean releaseTtlValueReferenceAfterRun; private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { //從父類copy值到本類當中 this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy()); this.runnable = runnable;//提交的runable,被修飾對象 this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; } /** * wrap method {@link Runnable#run()}. */ @Override public void run() { Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get(); if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } //裝載到當前線程 Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied); try { runnable.run();//執行提交的task } finally { //clear TransmittableThreadLocal.restoreBackup(backup); } } } 在上面的使用線程池的例子當中,如果換成這種修飾的方式進行操作,B任務得到的肯定是父線程中ThreadLocal的值,解決了在線程池中InheritableThreadLocal不能解決的問題。 更新父線程ThreadLocal值? 如果線程之間出了要能夠得到父線程中的值,同時想更新值怎么辦呢?在前面我們有提到,當子線程copy父線程的ThreadLocalMap的時候是淺拷貝的,代表子線程Entry里面的value都是指向的同一個引用,我們只要修改這個引用的同時就能夠修改父線程當中的值了,比如這樣: @Override public void run() { System.out.println("========"); Span span= inheritableThreadLocal.get(); System.out.println(span); span.name="liuliuliu";//修改父引用為liuliuliu inheritableThreadLocal.set(new Span("zhangzhangzhang")); System.out.println(inheritableThreadLocal.get()); } 這樣父線程中的值就會得到更新了。能夠滿足父線程ThreadLocal值的實時更新,同時子線程也能共享父線程的值。不過場景倒是不是很常見的樣子。

 

介紹InheritableThreadLocal之前,假設對 ThreadLocal 已經有了一定的理解,比如基本概念,原理,如果沒有,可以參考:ThreadLocal源碼分析解密.在講解之前我們先列舉有關ThreadLocal的幾個關鍵點 每一個Thread線程都有屬於自己的ThreadLocalMap,里面有一個弱引用的Entry(ThreadLocal,Object),如下 1 2 3 4 Entry(ThreadLocal k, Object v) { super(k); value = v; } 從ThreadLocal中get值的時候,首先通過Thread.currentThread得到當前線程,然后拿到這個線程的ThreadLocalMap。再傳遞當前ThreadLocal對象(結合上一點)。取得Entry中的value值 set值的時候同理,更改的是當先線程的ThreadLocalMap中Entry中key為當前Threadlocal對象的value值 Threadlocal bug? 如果子線程想要拿到父線程的中的ThreadLocal值怎么辦呢?比如會有以下的這種代碼的實現。由於ThreadLocal的實現機制,在子線程中get時,我們拿到的Thread對象是當前子線程對象,那么他的ThreadLocalMap是null的,所以我們得到的value也是null。 1 2 3 4 5 6 7 8 9 10 11 12 final ThreadLocal threadLocal=new ThreadLocal(){ @Override protected Object initialValue() { return "xiezhaodong"; } }; new Thread(new Runnable() { @Override public void run() { threadLocal.get();//NULL } }).start(); InheritableThreadLocal實現 那其實很多時候我們是有子線程獲得父線程ThreadLocal的需求的,要如何解決這個問題呢?這就是InheritableThreadLocal這個類所做的事情。先來看下InheritableThreadLocal所做的事情。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class InheritableThreadLocal extends ThreadLocal { protected T childValue(T parentValue) { return parentValue; } /** * 重寫Threadlocal類中的getMap方法,在原Threadlocal中是返回 *t.theadLocals,而在這么卻是返回了inheritableThreadLocals,因為 * Thread類中也有一個要保存父子傳遞的變量 */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * 同理,在創建ThreadLocalMap的時候不是給t.threadlocal賦值 *而是給inheritableThreadLocals變量賦值 * */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } 以上代碼大致的意思就是,如果你使用InheritableThreadLocal,那么保存的所有東西都已經不在原來的t.thradLocals里面,而是在一個新的t.inheritableThreadLocals變量中了。下面是Thread類中兩個變量的定義 1 2 3 4 5 6 7 8 9 /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; Q:InheritableThreadLocal是如何實現在子線程中能拿到當前父線程中的值的呢? A:一個常見的想法就是把父線程的所有的值都copy到子線程中。 下面來看看在線程new Thread的時候線程都做了些什么? 1 2 3 4 5 6 7 8 9 private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { //省略上面部分代碼 if (parent.inheritableThreadLocals != null) //這句話的意思大致不就是,copy父線程parent的map,創建一個新的map賦值給當前線程的inheritableThreadLocals。 this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); //ignore } 而且,在copy過程中是淺拷貝,key和value都是原來的引用地址 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { ThreadLocal key = e.get(); if (key != null) { Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } } 恩,到了這里,大致的解釋了一下InheritableThreadLocal為什么能解決父子線程傳遞Threadlcoal值的問題。 在創建InheritableThreadLocal對象的時候賦值給線程的t.inheritableThreadLocals變量 在創建新線程的時候會check父線程中t.inheritableThreadLocals變量是否為null,如果不為null則copy一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去 因為復寫了getMap(Thread)和CreateMap()方法,所以get值得時候,就可以在getMap(t)的時候就會從t.inheritableThreadLocals中拿到map對象,從而實現了可以拿到父線程ThreadLocal中的值 so,在最開始的代碼示例中,如果把ThreadLocal對象換成InheritableThreadLocal對象,那么get到的字符會是“xiezhaodong”而不是NULL InheritableThreadLocal還有問題嗎? 問題場景 我們在使用線程的時候往往不會只是簡單的new Thrad對象,而是使用線程池,當然線程池的好處多多。這里不詳解,既然這里提出了問題,那么線程池會給InheritableThreadLocal帶來什么問題呢?我們列舉一下線程池的特點: 為了減小創建線程的開銷,線程池會緩存已經使用過的線程 生命周期統一管理,合理的分配系統資源 對於第一點,如果一個子線程已經使用過,並且會set新的值到ThreadLocal中,那么第二個task提交進來的時候還能獲得父線程中的值嗎?比如下面這種情況(雖然是線程,用sleep盡量讓他們串行的執行): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 final InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal(); inheritableThreadLocal.set(new Span("xiexiexie")); //輸出 xiexiexie Object o = inheritableThreadLocal.get(); Runnable runnable = new Runnable() { @Override public void run() { System.out.println("========"); inheritableThreadLocal.get(); inheritableThreadLocal.set(new Span("zhangzhangzhang"); inheritableThreadLocal.get(); } }; ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.submit(runnable); TimeUnit.SECONDS.sleep(1); executorService.submit(runnable); TimeUnit.SECONDS.sleep(1); System.out.println("========"); Span span = inheritableThreadLocal.get(); } static class Span { public String name; public int age; public Span(String name) { this.name = name; } } 輸出的會是 1 2 3 4 5 6 7 8 9 xiexiexie ======== xiexiexie zhangzhangzhang ======== zhangzhangzhang zhangzhangzhang ======== xiexiexie 造成這個問題的原因是什么呢,下圖大致講解一下整個過程的變化情況,如圖所示,由於B任務提交的時候使用了,A任務的緩存線程,A緩存線程的InheritableThreadLocal中的value已經被更新成了”zhangzhangzhang“。B任務在代碼內獲得值的時候,直接從t.InheritableThreadLocal中獲得值,所以就獲得了線程A中心設置的值,而不是父線程中InheritableThreadLocal的值。 so,InheritableThreadLocal還是不能夠解決線程池當中獲得父線程中ThreadLocal中的值。 造成問題的原因 那么造成這個問題的原因是什么呢?如何讓任務之間使用緩存的線程不受影響呢?實際原因是,我們的線程在執行完畢的時候並沒有清除ThreadLocal中的值,導致后面的任務重用現在的localMap。 解決方案 如果我們能夠,在使用完這個線程的時候清除所有的localMap,在submit新任務的時候在重新重父線程中copy所有的Entry。然后重新給當前線程的t.inhertableThreadLocal賦值。這樣就能夠解決在線程池中每一個新的任務都能夠獲得父線程中ThreadLocal中的值而不受其他任務的影響,因為在生命周期完成的時候會自動clear所有的數據。Alibaba的一個庫解決了這個問題github:alibaba/transmittable-thread-local transmittable-thread-local實現原理 如何使用 這個庫最簡單的方式是這樣使用的,通過簡單的修飾,使得提交的runable擁有了上一節所述的功能。具體的API文檔詳見github,這里不再贅述 1 2 3 4 5 6 7 8 9 10 TransmittableThreadLocal parent = new TransmittableThreadLocal(); parent.set("value-set-in-parent"); Runnable task = new Task("1"); // 額外的處理,生成修飾了的對象ttlRunnable Runnable ttlRunnable = TtlRunnable.get(task); executorService.submit(ttlRunnable); // Task中可以讀取, 值是"value-set-in-parent" String value = parent.get(); 原理簡述 這個方法TtlRunnable.get(task)最終會調用構造方法,返回的是該類本身,也是一個Runable,這樣就完成了簡單的裝飾。最重要的是在run方法這個地方。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public final class TtlRunnable implements Runnable { private final AtomicReference<map<transmittablethreadlocal<?>, Object>> copiedRef; private final Runnable runnable; private final boolean releaseTtlValueReferenceAfterRun; private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { //從父類copy值到本類當中 this.copiedRef = new AtomicReference<map<transmittablethreadlocal<?>, Object>>(TransmittableThreadLocal.copy()); this.runnable = runnable;//提交的runable,被修飾對象 this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; } /** * wrap method {@link Runnable#run()}. */ @Override public void run() { Map<transmittablethreadlocal<?>, Object> copied = copiedRef.get(); if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } //裝載到當前線程 Map<transmittablethreadlocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied); try { runnable.run();//執行提交的task } finally { //clear TransmittableThreadLocal.restoreBackup(backup); } } } 在上面的使用線程池的例子當中,如果換成這種修飾的方式進行操作,B任務得到的肯定是父線程中ThreadLocal的值,解決了在線程池中InheritableThreadLocal不能解決的問題。 更新父線程ThreadLocal值? 如果線程之間出了要能夠得到父線程中的值,同時想更新值怎么辦呢?在前面我們有提到,當子線程copy父線程的ThreadLocalMap的時候是淺拷貝的,代表子線程Entry里面的value都是指向的同一個引用,我們只要修改這個引用的同時就能夠修改父線程當中的值了,比如這樣: 1 2 3 4 5 6 7 8 9 @Override public void run() { System.out.println("========"); Span span= inheritableThreadLocal.get(); System.out.println(span); span.name="liuliuliu";//修改父引用為liuliuliu inheritableThreadLocal.set(new Span("zhangzhangzhang")); System.out.println(inheritableThreadLocal.get()); } 這樣父線程中的值就會得到更新了。能夠滿足父線程ThreadLocal值的實時更新,同時子線程也能共享父線程的值。不過場景倒是不是很常見的樣子。 --------------------- 本文來自 騷年編程去 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/a837199685/article/details/52712547?utm_source=copy 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM