从ThreadLocal到TransmittableThreadLocal

971 阅读3分钟

ThreadLocal 基本使用

ThreadLocal类提供了线程局部变量,我们可以通过get、set方法来保存、访问只有当前线程才能操作的变量

实现原理

从他的ThreadLocal.set(T value)中看实现

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程中的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //不为空 新增
        map.set(this, value);
    else
        //为空 初始化
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

每个线程维护一个ThreadLocalMap对象,key对应的是当前ThreadLocal、value则是你想要存放的数据,这样 当你想要取出指定ThreadLocal数据的时候,只需要从ThreadLocalMap 中去拿出指定ThreadLocal的值,看实现:

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //通过当前ThreadLocal来拿值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    //没有初始化的话,执行初始化,返回null
    return setInitialValue();
}

ThreadLocalMap

那么ThreadLocalMap具体怎么保存(ThreadLocal->Value)数据的呢

static class ThreadLocalMap {
    //内部维护一个Entry数组
    private Entry[] table;

    //弱引用会在下面说
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ...
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化Entry数组
        table = new Entry[INITIAL_CAPACITY];
        //通过ThreadLocal计算数组下表
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        //设置阈值 INITIAL_CAPACITY(16)*2/3
        setThreshold(INITIAL_CAPACITY);
    }

    ...
}

类似HashMap的实现,简易很多,大致是内部维护一个Entry[], 通过ThreadLocal可以计算出唯一下标,给指定下标的Entry赋值,获取的时候计算出下标就可以直接拿到值了

内存泄漏问题

上面看Entry存储的时候ThreadLocal会以弱引用的形式存在,当把ThreadLocal置为空,垃圾回收时候会将ThreadLocal原内存块回收掉,这样就会有个key为空 value有值的Entry。

这个Entry 还有当前线程在持有,所以不会回收掉。一般情况下,随着线程消亡,这个value也会被清除掉。使用线程池的情况下,同一线程一直被重复使用,每次使用过后都会出现一条key为空 value有值的Entry,这样就会发生内存泄漏。

但是翻源码发现,ThreadLocalMap在set、get 都会执行expungeStaleEntry()方法清理过期的数据,这样除非极端情况。你这个线程不在运行了,或者不在执行ThreadLocal逻辑了,否则并不会出现内存泄漏的情况

InheritableThreadLocal

ThreadLocal是为了绑定当前线程,如果希望当前线程的ThreadLocal能被子线程使用,用户就需要在自己代码中实现传递,因此InheritableThreadLocal 就产生了

实现:

Thread类中

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

inheritableThreadLocals变量就是为了可自动向子线程传递的ThreadLocalMap, 有了这个map之后,只需要在父线程创建子线程的时候,将父线程的inheritableThreadLocals拿到,赋值给子线程的inheritableThreadLocals就可以实现传递了

源码如下:

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

从上述两个ThreadLocal看,是通过Thread配合实现的

TransmittableThreadLocal

InheritableThreadLocal是在new Thread()的时候,实现继承inheritableThreadLocals的,这种情况下,如果遇到线程池,线程循环使用,继承就不在可用了,下面看个InheritableThreadLocal使用失效的例子

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

public static void main(String[] args) throws InterruptedException {

    ExecutorService executorService = new ThreadPoolExecutor(1, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<>());

    new Thread(() -> {
        inheritableThreadLocal.set("Thread-1");
        executorService.execute(() -> {
            System.out.println("super-thread-1 current:" + Thread.currentThread().getName() + " value:" + inheritableThreadLocal.get());
        });
    }, "super-thread-1").start();

    Thread.sleep(1000);

    new Thread(() -> {
        inheritableThreadLocal.set("Thread-2");
        executorService.execute(() -> {
            System.out.println("super-thread-2 current:" + Thread.currentThread().getName() + " value:" + inheritableThreadLocal.get());
        });
    }, "Thread-2").start();

}

super-thread-1 current:pool-1-thread-1 value:Thread-1
super-thread-2 current:pool-1-thread-1 value:Thread-1

再此背景下TransmittableThreadLocal就产生了,他有几种模式来实现父子线程关系的传递

  • 修饰Runnable和Callable
  • 修饰线程池
  • 使用Java Agent来修饰JDK线程池实现类

它是将 Runnable和Callable装饰了一下,在创建Runnable或Callable的时候(创建Runnable或Callable时绝对是在父线程内),获取父线程需要传递的值,并复制一份到内存中,在执行run()/call()的时候,replay父线程的值,等执行完成之后 在restore为之前的值