InheritableThreadLocal與阿里的TransmittableThreadLocal設計思路解析

前言

參考文章:

  • 《全鏈路跟蹤(壓測)必備基礎組件之線程上下文“三劍客”》-- 原創: 丁威 中間件興趣圈
    https://mp.weixin.qq.com/s/a6IGrOtn1mi0r05355L5Ng
  • 《阿里巴巴Transmittable ThreadLocal(TTL) github》
    https://github.com/alibaba/transmittable-thread-local
  • 《TransmittableThreadLocal類的源碼》
    https://github.com/alibaba/transmittable-thread-local/blob/master/src/main/java/com/alibaba/ttl/TransmittableThreadLocal.java

ThreadLocal詳解的文章之前有寫過,但每次寫後的感覺都不一樣。

這裏我不對它進行詳細解析。

而是選擇對ThreadLocal核心功能的解釋,即忽略所有 get set 等方法的解釋。從而領略該類的設計思路。

我們知道大多數變量默認是所有線程都能訪問到的,只需要能傳遞引用即可。即任意線程訪問該【引用】都可以獲得唯一的一個【內存對象】。(注意粗體)

而如果我們希望創建一個【引用】,確保每一個線程訪問該【引用】都可以獲得專屬的【內存對象】(不再是唯一的,也不再是一樣的)。

我們還可以理解爲:過去訪問一個【目標內存對象】,只需要獲得這個【對象引用】作爲key即可。而對於ThreadLocal類型的對象引用 的【目標內存對象】 來說,你需要提供“線程對象引用”與“**目標內存對象的ThreadLocal對象引用 **”兩個索引,然後通過Map接口纔可以訪問得到。

再次強調理解上面這一點很重要。因爲下面講的內容是基於上面的理解,建議反覆讀上面的內容,否則會嚴重影響對下文的理解。

其次,從本質上理解,ThreadLocal對象值是和線程一一對應,即ThreadLocal對象值是線程的對象成員變量。


inheritableThreadLocals本質上和ThreadLocal沒什麼區別。

只是說inheritableThreadLocals的初始化過程發生在線程被構造的時候,即執行Thread#init()的時候。

到這裏我可以簡單歸納出:inheritableThreadLocals、ThreadLocal、TransmittableThreadLocal的本質的區別就是對象值實際引用聲明的位置以及初始化該值的時機。

  • ThreadLocal:變量值引用的位置在Thread類中聲明,初始化的動作是在線程對該變量進行get() or set()的時候觸發的。可以理解爲“懶漢模式”。

  • inheritableThreadLocals:其設計的初衷是爲了增強ThreadLocal類型,使其具備變量可以被子線程繼承的特性,具體表現爲當前線程創建子線程的時候,會把這一刻的ThreadLocal集合拷貝一份到子線程的ThreadLocal集合去, 注意!這裏說的拷貝並沒有說一定是淺拷貝或者是深拷貝,默認則是淺拷貝,可以通過重寫ThreadLocal類的T childValue(T parentValue)這個接口來實現深拷貝。

  • TransmittableThreadLocal: 變量值引用的位置可以看作實際上也在Thread類中聲明。至於初始化時機,爲了進一步增強inheritableThreadLocals,使其能夠在提交任務到線程池的時候拷貝“任何提交者(通常爲主線程)”的線程變量,因此會在當前線程創建任務的時候初始化,即構造Runnable接口的對象時初始化。

// 在TransmittableThreadLocal類中聲明,但由於其爲InheritableThreadLocal類型成員,所以最終還是可以看作是在Thread聲明,理解這一點很重要,因爲所有的ThreadLocal變量最終都是會集聚與各自的Thread對象內存中。

// Note about holder:
// 1. The value of holder is type Map<TransmittableThreadLocal<?>, ?> (WeakHashMap implementation),// but it is used as *set*.
// 2. WeakHashMap support null value.

private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {  

@Overrideprotected Map<TransmittableThreadLocal<?>, ?> initialValue() {

    return new WeakHashMap<TransmittableThreadLocal<?>, Object>();

}

@Overrideprotected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {

    return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);

}
}


以上只是比較簡單的理解,要實現完整的功能還有很多細節上的問題要處理,如果我們知道,對於一個線程來說不管是ThreadLocal,還是InheritableThreadLocal,或者是TransmittableThreadLocal,他們都是ThreadLocal。因爲TransmittableThreadLocal繼承的是InheritableThreadLocal,而InheritableThreadLocal繼承的是ThreadLocal。

而對於Thread來說它都會將他們一視同仁爲ThreadLocal。

那麼問題來了,我們清楚他們三者再初始化的時機是不一樣的,那麼Thread是怎麼區分他們的實際類型,從而能夠正確的時機對他們分別執行初始化的呢?當然是通過成員引用(變量名)啦,

ThreadLocal類型的變量會被下面Thread成員引用標記

/* ThreadLocal values pertaining to this thread. This map is 
maintained * by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;

InheritableThreadLocal類型的變量會被下面Thread成員引用標記

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

TransmittableThreadLocal類型的變量的標記方式卻有些不一樣,實際上也允許這種不一樣的設計方式。

這麼理解吧,我們知道TransmittableThreadLocal類型的變量本質上,是實實在在的ThreadLocal類型內存對象,我們只是爲了給這個對象加個標記,確認它是屬於具備有Transmittable能力的。要標記一個類,比較容易想到的方法就是,給這個類本身新增一個Type字段,還有一個方法就是:定義一個靜態全局變量集合,然後把具備Transmittable能力的ThreadLocal類型的引用都添加到集合裏面去,之後凡是屬於這個集合裏面的ThreadLocal類型的變量都可以被認爲被標記爲具備Transmittable能力的。

事實上,TransmittableThreadLocal類型的標記方式正是定義一個靜態全局變量集合,並約束只能通過【public static class Transmitter】對該靜態變量進行訪問。

但是這裏還有一個細節我們需要注意的,也是很容易被我忽略的,表現上看,
【private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder】是一個靜態類,即整個JVM裏面只有一份變量,實際上不是的,因爲InheritableThreadLocal類型,意味着,每一個線程有且只有一份這個Holder, 也就等於每一個線程對象成員變量的意思了。如果不好理解的話,我們可以假設去掉static,會有什麼後果?後果就是,假如我們new了兩個TransmittableThreadLocal對象,那麼每一個對象都會有一個holder成員,等價於每一個線程都有2個holder成員了(不再是有且只有一份了),所以說理解這個static也是很重要的。

到這裏,我們還有一個問題要處理,怎麼複製【Runnable提交者線程】的thread的TransmittableThreadLocal類型的變量的value值?

InheritableThreadLocal的核心源碼分析

在研究thread的TransmittableThreadLocal這個問題前,我建議最好先研究一下怎麼複製 InheritableThreadLocal的值先吧,畢竟這個比較規範,也比較好理解。之後我們即便不看TransmittableThreadLocal的源碼,只是簡單推理一下,也能知道它的源碼怎麼寫的。

首先我們知道,每一個線程對象都有一個成員變量 ”ThreadLocal.ThreadLocalMap inheritableThreadLocals; “他是一個Map,通過把對象引用作爲key傳入這個map就可以讀取這個對象引用對應在該線程的實際變量值。

如下的僞代碼所示:

   // 全局定義的ThreadLocal類型的對象引用;
  ThreadLocal<String> varKey = new ThreadLocal<String>();
  // 讀取該線程的ThreadLocal變量的實際值
  String myValue = thread. inheritableThreadLocals.get( varKey );
  

如果要複製這個值,則需要通過上面的方法讀取上一個線程的值,然後設置到新線程的Map裏面去,而Key則是同一個。僞代碼如下:

   // 全局定義的ThreadLocal類型的對象引用;
  ThreadLocal<String> varKey = new ThreadLocal<String>();
  // 讀取該線程的ThreadLocal變量的實際值
  String myValue = thread. inheritableThreadLocals.get( varKey );
  // 把值設置給新線程
  newThread.inheritableThreadLocals.put( varKey,  myValue );

以上只是簡單的僞代碼,實際的代碼如下:

起始代碼位於Thread對象的init方法中,爲什麼會放在這個地方呢?因爲inheritableThreadLocals複製父線程ThreadlocalMap的時機就是初始化新線程的時候執行的。

 private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
                      
                      ......省略不相干代碼......
                      
                        if (parent.inheritableThreadLocals != null)    
                                    this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
            
                      ......省略不相干代碼.....
                      
     
     }

核心代碼如下:

位於ThreadLocal類中:

    private ThreadLocalMap(ThreadLocalMap parentMap) {   
    
            Entry[] parentTable = parentMap.table;   
            int len = parentTable.length;    
            setThreshold(len);    
            table = new Entry[len];    
            // 遍歷源Map集合
            for (int j = 0; j < len; j++) { 
                    // 複製每一個ThreadLocal對象
                    Entry e = parentTable[j];        
                    if (e != null) { 
                            //  讀取key值
                            ThreadLocal key = e.get();            
                            if (key != null) { 
                            
                      // 【【 注意:這裏就是讀取Threadlocal值得地方,默認是淺拷貝這個value,可以重寫childValue方法改爲深拷貝】】
                                    Object value = key.childValue(e.value);    
                                    
                                    Entry c = new Entry(key, value);                
                                    int h = key.threadLocalHashCode & (len - 1);                
                                    //  採用挪位的方法來處理hashcode衝突的問題(Hashmap則採用鏈表和紅黑樹的方式)
                                    while (table[h] != null)                    
                                            h = nextIndex(h, len);                
                                    able[h] = c;                
                                    size++;           
                            }        
                    }    
         }
 }



好了線程回到最初的問題:

怎麼複製【Runnable提交者線程】的thread的TransmittableThreadLocal類型的變量的value值?

當然和inheritableThreadLocals的研究思路,我們先思考什麼時候複製,再找到複製代碼來學習。

TransmittableThreadLocal類型的變量什麼時候複製?

在這裏插入圖片描述

從這張圖我們可以知道,當傳教TTLRunnable對象的時候就會 開始觸發 複製動作了 (實際上還沒觸發,只是簡單的從提交者線程上capture它的Threadlocal集合而已) 。

我們知道,當提交者線程有很多ThreadLocal對象,那麼我們怎麼區分哪些是需要再提交時觸發複製的TransmittableThreadLocal類型變量,答案很明顯,就是我們前提提及的被【private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder】被這個holder標記的引用就是了。

我們可以看看capture的邏輯


    @NonNullpublic 
    static Object capture() {
            // 創建副本容器Map
            Map<TransmittableThreadLocal<?>, Object> captured 
                        = new HashMap<TransmittableThreadLocal<?>, Object>();
             // 把提交者線程的holder中的Threadlocal集合遍歷複製到副本容器Map中。           
            for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
                    captured.put(threadLocal, threadLocal.copyValue());
            }
            return captured;
   }

我們知道了什麼時候開始複製了之後,我們還得搞清楚另一個事情,最終怎麼保存到holder的呢?這個問題相當複雜。我們先跳過,我們先理解另一個問題。

也就是captured獲得的值怎麼保存到ThreadLocal中,以及在線程池的線程執行完後怎麼Threadlocal又怎麼恢復回原來的值的呢?

這個問題其實不難,就跟上面InheritableThreadLocal一樣修改Map的值而已,無論是設置還是恢復都只是簡單的修改獲取。

直接看代碼:


@NonNull
public static Object replay(@NonNull Object captured {

        @SuppressWarnings("unchecked")
        Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured;
        Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>();
        // 遍理holder的目的是爲了備份thread之前的Holder狀態
        for (
            Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
            iterator.hasNext(); 
         ) {
                
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();
                 // backup 備份holder值
                 backup.put(threadLocal, threadLocal.get());
                 
                // capturedMap中的值是需要設置到子線程去的,所以capturedMap中的值應該是多於holder中的值,
                //  即Holder中不允許存在capture沒有的值,所以就需要把他去掉。
                // 你可能會問,如果holder現在爲什麼不標記captureMap中的值呢?因爲現在還不是時候,不需要那麼急着做這種事情。
                // clear the TTL values that is not in captured
                // avoid the extra TTL values after replay when run task
               if (!capturedMap.containsKey(threadLocal)) {
                         iterator.remove();
                        threadLocal.superRemove();
                }
         }
         // set TTL values to captured
         // 這裏很重要哦,這裏的代碼就是用來設置Threadlocal的值的代碼,上面的代碼我們主要目的只是備份Holder而已
         // 至於什麼時候把capture的值設置到holder中,這個暫時不重要
         setTtlValuesTo(capturedMap);
        // call beforeExecute callback
        doExecuteCallback(true);
                
       return backup;
  }
  
  
  // 設置ThreadLocal的值的代碼其實比較簡單,就只是簡單的設置Map的值而已。代碼如下:
  private static void setTtlValuesTo( @NonNull Map<TransmittableThreadLocal<?>, Object> ttlValues) {
  
        for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet())            
        {
                @SuppressWarnings("unchecked")
                TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
                // 這裏最終會設置到Thread對象的ThreadLocal.ThreadLocalMap threadLocals;成員中去。
                // threadLocal.set看起來很簡單,實際上包含了從Map中獲取key和put值的動作,
                // 這點基本知識可別忘了,代碼我在下面也給出來了
                threadLocal.set(entry.getValue());
       }
       
}
  
  
  // ThreadLocal對象的set方法,具體功能就是獲取當前對象的map集合,然後覆蓋key對應值。
  
  /** 
  * Sets the current thread's copy of this thread-local variable 
  * to the specified value.  Most subclasses will have no need to 
  * override this method, relying solely on the {@link #initialValue} 
  * method to set the values of thread-locals. 
  * 
  * @param value the value to be stored in the current thread's copy of 
  *        this thread-local. 
*/
public void set(T value) {    
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) 
                map.set(this, value);
       else
                createMap(t, value);}

}

分析到這裏,接近尾聲了,也就是,什麼時候設置holder值呢?

holder的意義我們已經非常清楚了,就是用來標記ThreadLocal對象,確保在capture方法執行時能夠給讀取並傳遞給線程池中的線程。
我們已經瞭解線程池線程從任務創建(capture捕捉)到執行(複製)的整個過程,但是到現在都還沒發現涉及設置holder值的方法,所以我們大概也猜到了要麼被延遲了執行get,set方法的時候,通過查看TransmittableThreadLocal類的get、set方法確實有這麼一段代碼,但這個理由依然不夠充,get、set的邏輯可能是對線程池在run任務而創建的新ThreadLocal假如holder的一個入口而已。

所以還有一個可能就是我們疏忽了上面的一個細節(上面廢話了一段只是湊字數看官不要惱怒),細節在於設置Capture集合的值給線程的ThreadLocal地方。代碼如下:


  // 設置ThreadLocal的值的代碼其實比較簡單,就只是簡單的設置Map的值而已。代碼如下:
  private static void setTtlValuesTo( @NonNull Map<TransmittableThreadLocal<?>, Object> ttlValues) {
  
        for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet())            
        {
                @SuppressWarnings("unchecked")
                // 正是這裏,我們看到了下面代碼,局部變量threadLocal的類型是TransmittableThreadLocal#set方法
                // 這意味着,threadLocal.set執行的不是ThreadLocal#set()方法,而是TransmittableThreadLocal#set方法
                // TransmittableThreadLocal#set方法則會將threadLocal設置到holder中了。Happy Ending (* ^_^ * )
                TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
                
                threadLocal.set(entry.getValue());
       }
       
}

// TransmittableThreadLocal#set方法 如下:


/*** see {@link InheritableThreadLocal#set}*/
@Override
public final void set(T value) {
        super.set(value);
        // may set null to remove value
        if (null == value) 
                removeValue();
        else 
                addValue();
 }
 
 //  關鍵代碼在這裏:addValue()的方法,如下
 
 private void addValue() {
        if (!holder.get().containsKey(this)) {
        
                   //【【 將ThrealLocal對象標記到holder集合中】】
                  holder.get().put(this, null); // WeakHashMap supports null value.
                  
        }
}


結語

TransmittableThreadLocal功能上,是比較完美的。但是你會發現代碼還是比較難以理解,這個可以理解爲抽象的不夠好。才增加了我們對源碼的閱讀難度,我們把TransmittableThreadLocal的源碼和InheritableThreadLocal做對比就很明顯,InheritableThreadLocal的源碼好理解很多。這個是不爭的事實,我們也可以猜測TransmittableThreadLocal源碼比較不好讀的原因爲:本身要實現傳遞給線程的ThreadLocal的需求就是很複雜的,其次是TransmittableThreadLocal畢竟是非官方的擴展類,而且官方並沒有提供友好的擴展口也導致代碼不好寫。不管怎樣,我們確實學習到了面向的對象的不少技巧和Java庫函數的知識。對吧?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章