深入解析ThreadLocal线程上下文原理

216 阅读4分钟

首先来看看ThreadLocal的创建

private static final ThreadLocal<String> USER_INFO = new ThreadLocal<>();

看看ThreadLocal对象的创建过程, 可能有些人说很简单就是new了个对象,但我觉得其实其中有些很重要的细节,我们看看他的成员变量和静态变量, 下面我只贴出了我们需要注意的部分源码

public class ThreadLocal<T> {
    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    ....省略。。。
    }
  1. private final int threadLocalHashCode = nextHashCode(); 这个成员的作用是什么呢, 其实你就把他理解为每个ThreadLocal实例对象的身份证,注意:每new一个新的对象, 它的值是唯一的, 为什么需要这个, 我下面会解释。
  2. private static final int HASH_INCREMENT = 0x61c88647; 可以看出它是私有的且唯一的, 外界不可修改。其实它是threadLocalHashCode的初始值。
  3. private static AtomicInteger nextHashCode = new AtomicInteger(); 线程安全的计数器, 其实就是用来生成threadLocalHashCode 以HASH_INCREMENT为初始值, 每次实例化一个ThreadLocal对象就加一。

总结 每次new ThreadLocal() 会为它本身赋值一个唯一身份标识threadLocalHashCode, 这个变量的初始值为HASH_INCREMENT, 每次实例化的时候, 就会调用nextHashCode() 加一。

使用ThreadLocal set方法存储值

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

看set源码, value参数就是你要存储的值, 首先会获取当前线程对象, 第二步获取当前线程的ThreadLocalMap 大家可以去看看getMap(t) 的源码,其实ThreadLocalMap 就是Thread类的成员变量。初始值为null。那么当你的线程第一次使用set方法的时候, 这个变量毫无疑问是null, 所以它会去调用 createMap(t, value); 接下来让我们看看它的源码

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

好家伙, 其实只是new ThreadLocalMap(this, firstValue) 然后赋值给当前线程对象对吧。 所以还要继续看看new ThreadLocalMap(this, firstValue) 干了啥, 为啥就能在ThreadLocal中保存线程所需要存储的变量呢?

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

我们看看ThreadLocalMap的构造器, 传入了当前ThreadLocal对象, 和你需要存储的value值。 第一步先初始化了一个Entry数组, 这个entry你就理解为一个键值对吧,key就是ThreadLocal对象, value就是你真实要存储的值。第二步, 其实就是拿了ThreadLocal对象的唯一身份Id(就是threadLocalHashCode), 然后计算出此ThreadLocal对象应该存储在Entry数组的索引值, 最后就是创建Entry对象, 放入Entry数组中。Entry对象存储了真实的value值。

总结: 在线程第一次使用ThreadLocal对象存储value值时, 干了这么几件事情。1.初始化线程的ThreadLocalMap对象 2. ThreadLocalMap对象创建对应的entry保存value值, 并根据ThraadLocal的唯一身份标识计算应该存入到entry数组的对应位置。这就是我们调用# ThreadLocal set方法所发生的事情。

使用 ThreadLocal get方法获取值

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
  1. 首先还是获取当前的线程对象
  2. 获取当前线程对象的 ThreadLocalMap 它就是threadLocal的一个属性而已, 上文已经描述过。
  3. 根据ThreadLocalMap 获取对应的entry 上文讲过, ThreadLocalMap会保存一个entry数组, 那么它会根据ThreadLocal的唯一身份标识计算出索引的位置,然后获取出来。
  4. 从entry对象中, 获取你真实存储的value值。

总结: 这就是我为什么说ThreadLocal的创建过程有些细节要了解。因为在整个过程中都是使用threadLocalHashCode 来决定entry应该放在数组的哪个位置。本篇是围绕线程第一次创建ThreadLocalMap来讲的, 其实若不是第一次创建(也就是你不止一个ThreadLocal对象), 那么只是往ThreadLocalMap的entry数组中, 添加你其他的ThreadLocal对象要存储的value值。

最后附上一张它们的关系图:

1676449141871.png

其实就是 一个Thread对象有一个对应的ThreadLocalMap对象的成员变量, 然后ThreadLocalMap对象里面保存了entry数组, 那么entry数组是根据ThreadLocal的唯一身份id计算出索引位置,保存对应的entry信息, entry信息里面就保存了你想要的value。那么这也就是你能创建N个不同类型的ThreadLocal,保存不同类型的value值挂到线程上的原因。