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, ?> (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, ?>> 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类型的变量什么时候复制?

InheritableThreadLocal与阿里的TransmittableThreadLocal设计思路解析_第1张图片

从这张图我们可以知道,当传教TTLRunnable对象的时候就会 开始触发 复制动作了 (实际上还没触发,只是简单的从提交者线程上capture它的Threadlocal集合而已) 。

我们知道,当提交者线程有很多ThreadLocal对象,那么我们怎么区分哪些是需要再提交时触发复制的TransmittableThreadLocal类型变量,答案很明显,就是我们前提提及的被【private static InheritableThreadLocal, ?>> 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库函数的知识。对吧?

你可能感兴趣的:(java,编程)