引出

我们知道,ThreadLocal 设计初衷是为了在多线程环境下,针对每一个线程能有一个自己的副本,这样可以在一定程度上解决多线程并发修改的问题。
但是,我们可以在此基础上做一个拓展,比如context,我们可以利用 ThreadLocal 针对每一个线程都有一个自己的上下文,一般都是写成ThreadLocal<Context>,这样在这个线程上做的所有修改都可以被大家利用到。这是我们项目中经常用的的,比如会把经常用到的用户id存储在我们的上下文当中。比如前端携带token来访问我们的接口请求,我们拿到token解析,并将用户的id存在context中,然后你再接口中就可以直接拿到这个用户id来使用了。
此时设想一下,假如我们新建一个子线程,那这个子线程可以获取到父线程的context吗?理论上希望可以达成这样的效果,实际上呢?让我们看看:

  1. /**
  2. * @author heian
  3. * @create 2021-01-17-10:47 下午
  4. */
  5. public class ThreadLocalContext {
  6. private static ThreadLocal<Context> context = new ThreadLocal<>();
  7. static class Context {
  8. String name;
  9. int value;
  10. }
  11. public static void main(String[] args) {
  12. Context context = new Context();
  13. context.name = "mainName";
  14. context.value = 10;
  15. ThreadLocalContext.context.set(context);
  16. new Thread(() -> {
  17. Context childContext = ThreadLocalContext.context.get();
  18. System.out.println(childContext.name);
  19. System.out.println(childContext.value);
  20. }).start();
  21. }
  22. }

运行 main 方法之后,直接在子线程中抛错,这样确实符合我们的预期,但如果我们想达到子线程可以获取到父线程的 context这样的效果该如何做呢?首先想到的就是在生成子线程的时候,将父线程 ThreadLocal 里的值传给子线程。这样做虽然能达到效果,但过程比较繁杂,且代码侵入性强。这个时候就可以用InheritableThreadLocal了。

分析源码

先让我们看看它的源码,大家不要怕,它的源码很少:

package java.lang;
import java.lang.ref.*;

/**
 * @author  Josh Bloch and Doug Lea
 * @see     ThreadLocal
 * @since   1.2
 */

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

    protected T childValue(T parentValue) {
        return parentValue;
    }


    ThreadLocalMap getMap(Thread t) {
        //返回的其实是Thread对象的inheritableThreadLocals属性
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
         // 也是给Thread的inheritableThreadLocals属性赋值
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

首先它继承自 ThreadLocal,那么它其实就是 ThreadLocal 的一个拓展版本,接下来就是这三个方法,其实这三个方法在 ThreadLocal 都是有的,我们来看看:

T childValue(T parentValue) {
    throw new UnsupportedOperationException();
}

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

除了childValue方法在 ThreadLocal 中是抛出异常的,其余两个方法在两个类中都几乎是一样,只是针对的对象不同而已,但threadLocals和inheritableThreadLocals都是ThreadLocal.ThreadLocalMap类型(Thread类),就是一个 key 为弱引用的 Entry,这个倒不是重点。我们再来看看 inheritableThreadLocals 是在何时被初始化的,从源码可以得知:
image.png

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
                // 省略无关代码
                ...
                Thread parent = currentThread();
                ...
                // 省略无关代码
                ...
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
                ...
        }

当我们通过父线程调用 Thread 的构造方法生成一个子线程时,其构造方法最终会调用这个 init 方法。从这儿可以看出, inheritableThreadLocals 是来自于父线程的 inheritableThreadLocals,那这样也就解释了为什么 inheritableThreadLocals 支持在子线程中使用父线程中存储的变量。
所以上面的变量只需要改成private static InheritableThreadLocal<Context> context = new InheritableThreadLocal<>();就可以拿到父线程所创建的变量了。

引发问题

父线程的ThreadLocalMap(下面简称TLMap)里的值递给了子线程(指向TLMap的引用而已),父子线程里指向的均是同一个对象,因此任意线程改了这个值,对其他线程是可见的,为了验证这一点,我们可以改造以上测试代码:

package com.dongnaoedu.network.humm.多线程.threadLocal;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.concurrent.locks.LockSupport;

/**
 * @author heian
 * @create 2021-01-17-10:47 下午
 */
public class InThreadLocalContext {

    private static InheritableThreadLocal<ContextObject> inTl = new InheritableThreadLocal<>();

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class ContextObject {
        String name;
        int value;
    }

    public static void main(String[] args) {

        ContextObject context = new ContextObject();
        context.setName("father");
        context.setValue(10);
        inTl.set(context);

        new Thread(() -> {
            // 获取线程的上下文
            ContextObject contextObject = inTl.get();
            if (contextObject != null) {
                System.out.println("son-1:" + contextObject.getName() + "," + contextObject.getValue());
            }
            inTl.set(new ContextObject("son-1",21));
            System.out.println("son-1:" + inTl.get().getName() + "," + inTl.get().getValue());
        }).start();

        LockSupport.parkNanos(1000*1000*1000L);

        System.out.println("-----");
        System.out.println(inTl.get().getName() + "," + inTl.get().getValue());
    }
}
/**
 * son-1:father,10
 * son-1:son-1,21
 * -----
 * father,10
 */

可以确认,子线程里持有的本地变量跟父线程里那个是同一个对象。通过上述的测试代码,基本可以确定父线程的TLMap被传递到了下一级,那么我们基本可以确认ITL是TL派生出来专门解决线程本地变量父传子问题的,那么下面通过源码来分析一下ITL到底是怎么完成这个操作的。
先来了解下Thread类,上节说到,其实最终线程本地变量是通过TLMap存储在Thread对象内的,那么来看下Thread对象内关于TLMap的两个属性:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Thread类里其实有两个TLMap属性,第一个就是普通TL对象为其赋值,第二个则由ITL对象为其赋值,来看下TL的set方法的实现,这次针对该方法介绍下TL子类的相关方法实现:

// TL的set方法,如果是子类的实现,那么获取(getMap)和初始化赋值(createMap)都是ITL对象里的方法
// 其余操作不变(因为hash计算、查找、扩容都是TLMap里需要做的,这里子类ITL只起到一个为Thread对象里哪个TLMap属性赋值的作用)
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

而inheritableThreadLocals里的信息通过Thread的init方法是可以被传递下去的:

// 初始化一个Thread对象时的代码段(Thread类的init方法)
    Thread parent = currentThread();
    if (parent.inheritableThreadLocals != null){ //可以看到,如果父线程存在inheritableThreadLocals的时候,会赋值给子线程(当前正在被初始化的线程)
        // 利用父线程的TLMap对象,初始化一个TLMap,赋值给自己的inheritableThreadLocals(这就意味着这个TLMap里的值会一直被传递下去)
        this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }

    // 看下TL里对应的方法
    static ThreadLocal.ThreadLocalMap createInheritedMap(ThreadLocal.ThreadLocalMap parentMap) {
        return new ThreadLocal.ThreadLocalMap(parentMap); //这里就开始初始化TLMap对象了
    }

    // 根据parentMap来进行初始化子线程的TLMap对象
    private ThreadLocalMap(ThreadLocal.ThreadLocalMap parentMap) {
        ThreadLocal.ThreadLocalMap.Entry[] parentTable = parentMap.table; //拿到父线程里的哈希表
        int len = parentTable.length;
        setThreshold(len); // 设置阈值(具体方法参考上一篇)
        table = new ThreadLocal.ThreadLocalMap.Entry[len];

        for (int j = 0; j < len; j++) {
            ThreadLocal.ThreadLocalMap.Entry e = parentTable[j]; //将父线程里的Entry取出
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); //获取key
                if (key != null) {
                    Object value = key.childValue(e.value); //获取value
                    ThreadLocal.ThreadLocalMap.Entry c = new ThreadLocal.ThreadLocalMap.Entry(key, value); //根据k-v重新生成一个Entry
                    int h = key.threadLocalHashCode & (len - 1); //计算哈希值
                    while (table[h] != null)
                        h = nextIndex(h, len); //线性探查解决哈希冲突问题(具体方法参考上一篇)
                    table[h] = c; //找到合适的位置后进行赋值
                    size++;
                }
            }
        }
    }

    // ITL里的childValue的实现
    protected T childValue(T parentValue) {
        return parentValue; //直接将父线程里的值返回
    }

看过上述代码后,现在关于ITL的实现我们基本上有了清晰的认识了,根据其实现性质,可以总结出在使用ITL时可能存在的问题:

线程不安全

如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量(本质上是同一个对象),参考上面的第三个例子,子线程写入后会覆盖掉主线程的变量,也是通过这个结果,我们确认了子线程TLMap里变量指向的对象和父线程是同一个。

线程池中可能失效

按照上述实现,在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过init一个Thread的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了。

/**
 * @author heian
 * @create 2021-01-17-10:47 下午
 */
public class ThreadLocalContext3 {

    private static ExecutorService executorService = Executors.newFixedThreadPool(1);
    private static InheritableThreadLocal<Context> context = new InheritableThreadLocal<>();

    static class Context {
        String name;
        int value;
    }

    public static void main(String[] args) {
        Context context = new Context();
        context.name = "mainName";
        context.value = 10;
        ThreadLocalContext3.context.set(context);

        executorService.execute(() -> {
            Context childContext = ThreadLocalContext3.context.get();
            System.out.println(Thread.currentThread().getName() + "  " + childContext.name + "  " + childContext.value);
        });


        executorService.execute(() -> {
            Context childContext = ThreadLocalContext3.context.get();
            System.out.println(Thread.currentThread().getName() +  "  " + childContext.name + "  " + childContext.value);
        });
    }
}
pool-1-thread-1  mainName  10
pool-1-thread-1  mainName  10

但是发现其中并没有发生引用传递失效的问题,因为我们在执行任务的时候都发生了创建新的线程来执行任务,二此时父线程是有数据的会把数据传递给它的子线程,要想实现传递失效,只需要在创建线程时父线程暂无数据便可。

/**
 * @author heian
 * @create 2021-01-17-10:47 下午
 */
public class ThreadLocalContext3 {

    private static ExecutorService executorService = Executors.newFixedThreadPool(1);
    private static InheritableThreadLocal<Context> context = new InheritableThreadLocal<>();

    static class Context {
        String name;
        int value;
    }

    public static void main(String[] args) {


        executorService.execute(() -> {
            Context childContext = ThreadLocalContext3.context.get();
            System.out.println(Thread.currentThread().getName() +  "  " + childContext);
        });

        //执行第二步
        LockSupport.parkNanos(1000*1000*1000L);

        Context context = new Context();
        context.name = "mainName";
        context.value = 10;
        ThreadLocalContext3.context.set(context);


        executorService.execute(() -> {
            Context childContext = ThreadLocalContext3.context.get();
            System.out.println(Thread.currentThread().getName() +  "  " + childContext);
        });

        LockSupport.parkNanos(1000*1000*1000L);
        System.out.println(Thread.currentThread().getName() + ThreadLocalContext3.context.get());
    }
}
pool-1-thread-1  null
pool-1-thread-1  null
maincom.dongnaoedu.network.humm.多线程.ThreadLocal.ThreadLocalContext3$Context@2d209079

很明显,第一次启用时没有递进去的值,在后续的子线程启动时就再也传递不进去了。但是,在实际项目中我们大多数采用线程池进行做异步任务,假如真的需要传递主线程的本地变量,使用ITL的问题显然是很大的,因为是有极大可能性拿不到任何值的,显然在实际项目中,ITL的位置实在是尴尬,所以在启用线程池的情况下,不建议使用ITL做值传递。为了解决这种问题,阿里做了transmittable-thread-local(TTL)来解决线程池异步值传递问题,下一篇,我们将会分析TTL的用法及原理。

参考至:https://www.cnblogs.com/hama1993/p/10400265.html