08、InheritableThreadLocal(可继承的ThreadLocal)详解

一、前言

前一段时间做了个服务调用链路追踪的需求,最后需要把trace信息通过Mybatis Plugin持久化到每个业务表中;以供后面的日志审计服务使用。
其中我采用ThreadLocal把trace信息传递到Mybatis Plugin中,不过由于存在 在dubbo接口 / Rest接口中采用new Threa()、CompleteFuture的方式启动一个异步线程去做DB数据(走MyBatis)的持久化,这时ThreadLocal就无法将trace信息传递到Mybatis Plugin,因此有了今天InheritableThreadLocal的故事。

二、InheritableThreadLocal概述

InheritableThreadLocal用于实现 父线程生成的变量可以自动传递到子线程中进行使用 的需求。

 
从类的构造来看,InheritableThreadLocal继承自ThreadLocal

  • 在编码使用上,InheritableThreadLocal和ThreadLocal没有任何区别,只是在实例化的对象的时候使用new InheritableThreadLocal<>()。

1、ThreadLocal的使用

public class ParentChildThreadLocal {

    public final static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        threadLocal.set("haha, parent-child variables!");
        System.out.println("父线程的值(threadLocal):" + threadLocal.get());

        // 开启子线程
        new Thread(() -> {

            System.out.println("子线程继承到的值(threadLocal): " + threadLocal.get());
        }).start();

        try {

            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {

            e.printStackTrace();
        }
    }
}

上述代码中,实例化一个ThreadLocal变量,在main线程中对ThreadLocal变量进行赋值、取值,并在main线程中开启一个子线程获取main线程中ThreadLocal变量的值。执行结果如下:
&nbsp;

从运行结果可以看出,使用ThreadLocal时,子线程无法获取到父线程中的本地变量值。

2、InheritableThreadLocal的使用

public class ParentChildThreadLocal {

    public final static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {

        inheritableThreadLocal.set("haha, parent-child variables!");
        System.out.println("父线程的值(InheritableThreadLocal):" + inheritableThreadLocal.get());

        // 开启子线程
        new Thread(() -> {

            System.out.println("子线程继承到的值(InheritableThreadLocal): " + inheritableThreadLocal.get());
        }).start();

        try {

            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {

            e.printStackTrace();
        }
    }
}

上述代码中,实例化一个InheritableThreadLocal变量,在main线程中对InheritableThreadLocal变量进行赋值、取值,并在main线程中开启一个子线程获取main线程中InheritableThreadLocal变量的值。执行结果如下:

&nbsp;

从运行结果可以看出,使用InheritableThreadLocal之后,子线程可以获取到父线程中的本地变量值。

那么为什么呢?下面来跟一下InheritableThreadLocal的源码。

三、InheritableThreadLocal源码分析

经过上面的测试代码,我们可以推测出InheritableThreadLocal是ThreadLocal派生出专门用于解决线程本地变量父传子问题的,这里我们通过源码分析一下InheritableThreadLocal是如何完成这一操作的!

1、Thread类构成

&nbsp;
Thread类中包含 threadLocals 和 inheritableThreadLocals 两个变量,其中 inheritableThreadLocals 用于存储可自动从父线程向子线程中传递的ThreadLocal.ThreadLocalMap。

1> Thread#inheritableThreadLocals变量

接着,来看一下创建线程时如何实现ThreadLocal在父子线程之间的传递;
&nbsp;
而此处所谓的父线程,指想调用new Thread()实例化一个线程的当前线程。

2> ThreadLocal#createInheritedMap()

再看ThreadLocal#createInheritedMap()方法是如何将父线程中的inheritableThreadLocals传递到子线程的。

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {

        return new ThreadLocalMap(parentMap);
    }

本质上就是实例化一个ThreadLocalMap:

private ThreadLocalMap(ThreadLocalMap parentMap) {

    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    // ThreadLocalMap 使用 Entry[] table 存储ThreadLocal
    table = new Entry[len];

    // 逐一复制 parentMap中的所有ThreadLocal记录
    for (int j = 0; j < len; j++) {

        Entry e = parentTable[j];
        if (e != null) {

            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {

                // childValue()内部是直接将e.value返回
                Object value = key.childValue(e.value);
                // 构建Entry数组节点,放入到ThreadLocalMap中
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                // 使用开发地址方法解决hash冲突
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

由于ThreadLocalMap内部存在hash冲突的情况,所有采用开发地址法解决hash冲突问题。

  • 即数据想放在索引下标为3,但是索引下标3处存在数据,此时就从索引下标4开始往后查找空闲的位置。相应的它的查询速度就会慢很多,这是一种时间换空间的做法。

从这里可以推测出,InheritableThreadLocal主要便是针对Thread类的inheritableThreadLocals变量做操作。

2、InheritableThreadLocal类结构

首先,InheritableThreadLocal类继承自ThreadLocal;
&nbsp;
然后重写了ThreadLocal的三个方法:childValue(T parentValue)getMap(Thread t)createMap(Thread t, T firstValue)

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {

        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {

       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {

        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

1、ThreadLocal#set()方法

走到ThreadLocal#set()方法里:
&nbsp;
上面提到,InheritableThreadLocal重写了ThreadLocal的三个方法,其中就包括getMap(Thread t)createMap(Thread t, T firstValue);也就是说ThreadLocal存储数据的主体从Thread的threadLocals变量变成了inheritableThreadLocals变量;
&nbsp;
而对于其余操作并无区别;

  • 因为hash计算、查找、扩容都是ThradLocalMap里做的,
  • 而InheritableThreadLocal只决定为Thread对象里哪个ThreadLocalMap属性赋值。

2、ThreadLocal#get()方法

ThreadLocal#get()方法和set()方法一样,只是操作的Thread的ThradLocalMap对象不同。
&nbsp;

3、阶段总结,问:InheritableThreadLocal和ThreadLocal的区别是什么?

  1. InheritableThreadLocal继承自ThreadLocal,用于解决父子线程之间传递变量的问题。
  2. InheritableThreadLocal之所以能在父子线程之间传递变量,是因为其使用的存储线程ThreadLocal数据的主体ThreadLocalMap是Thread类中的inheritableThreadLocals,而ThreadLocal使用的是Thread类中的threadLocals

    • 在线程初始化的时候,会从父线程中取出inheritableThreadLocals信息传递到子线程。
  3. 所以InheritableThreadLocal和ThreadLocal的唯一区别在于:它俩存储数据的ThreadLocalMap主体不同。

三、InheritableThreadLocal有什么问题吗?

1、直接通过set改变对象内容时,线程不安全

如果线程本地变量是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量(本质上是同一个对象)。

这里对于共享变量的修改存在线程安全问题太正常了。主要点在于线程池中InheritableThreadLocal失效。

2、线程池中InheritableThreadLocal失效

在使用线程池时,InheritableThreadLocal会完全失效;因为父线程的ThreadLocalMap是通过实例化一个Thread时赋值给子线程的,而线程池在执行异步任务时可能不需要创建新的线程,因此也就不会再传递父线程的ThreadLocalMap给子线程。

针对这个问题,我们可以通过使用阿里开源的TransmittableThreadLocal解决,参考文档:https://github.com/alibaba/transmittable-thread-local
具体内容下篇博文输出。