ThreadLocal 源码解析(一)

前言

在开始之前先看下代码如下:

ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
new Thread(() -> {
    stringThreadLocal.set("测试");
    System.out.println("子线程获取:" + stringThreadLocal.get());
}).start();
Thread.sleep(200);
System.out.println("主线程获取:" + stringThreadLocal.get());
复制代码

输出如下:

子线程获取:测试
主线程获取:null
复制代码

是不是感觉哪里不对劲。按道理,如果是其他的类似于集合的数据结构,我只要在子线程进行了赋值操作,又在主线程执行了sleep()操作那么主线程应该是可以获取到最新的赋值的,比如将上面ThreadLocal改为ArraList,代码如下:

ArrayList<String> arrayList = new ArrayList<>();
new Thread(() -> {
    arrayList.add("测试");
    System.out.println("子线程获取:" + arrayList.get(0));
}).start();
Thread.sleep(200);
System.out.println("主线程获取:" + arrayList.get(0));
复制代码

输出如下:

子线程获取:测试
主线程获取:测试
复制代码

再看下如下代码:

ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.set("测试");
Thread.sleep(200);
new Thread(() -> {
    System.out.println("子线程获取:" + stringThreadLocal.get());
}).start();
System.out.println("主线程获取:" + stringThreadLocal.get());
复制代码

输出结果如下:

主线程获取:测试
子线程获取:null
复制代码

通过以上三个代码片段,似乎可以发现一些端倪,那就是ThreadLoal通过set()操作进行的赋值操作的作用范围似乎只能是在当前线程,那它是如何实现这种线程隔离的效果的呢? Talk is cheap,show me the fuck code.

正文

正文主要分亮大部分ThreadLocal本身的函数、与ThreadLocal息息相关的ThreadLocalMap

ThreadLocal本身

先看构造函数:

public ThreadLocal() {
}
复制代码

没啥特殊的,那就看下set()函数:

set()函数

public void set(T value) {
    //注释1
    Thread t = Thread.currentThread();
    //注释2
    ThreadLocalMap map = getMap(t);
    if (map != null)
    //注释4
        map.set(this, value);
    else
        //注释3
        createMap(t, value);
}
复制代码

注释1处获取当前线程t,然后把t当做参数通过getMap,获取ThreadLocalMap,如下:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
复制代码

返回的是是Thread的一个成员变量,那我们就来看看threadLocals初始化和赋值的地方如下:

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

可以发现threadLocals是在ThreadLocal#createMap()里面赋值的,再回到代码片段4的注释2显然一开始这里的map为null,逻辑走到了注释3的createMap(),也就是在这里完成了threadLocals的初始化以及赋值。关于ThreadLocalMap晚点再看,我们趁热打铁来看get()

get()

get()函数如下:

public T get() {
    //注释1
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
     //注释2
    if (map != null) {
     //注释3
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
     //注释4
    return setInitialValue();
}
复制代码

和set()一样,先获取当前线程然后通过getMap()获取ThreadLocalMap,因为前面已经调用过来set()函数,所以map不等于null,走到注释3,通过 ThreadLocalMap#getEntry()获取Entry并返回Entry的value,如果map等于走到注释4的逻辑 如下:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
复制代码

这里可以看做是走了以便set(null)的操作,并返回空值

小结

通过以上set()和get()方法的分析能够知道,两者都需要调用getMap()拿到threadLocals,再通过threadLocals去取值或者赋值,而threadLocals是Thread的成员变量,不同线程实例有自己的hreadLocals,天然形成了线程隔离,这也就能回答文章开头提出的问题。

ThreadLocalMap

通过源码分析知道ThreadLoal的各种操作与ThreadLocalMap紧密相关,那接下来我们看看ThreadLocalMap的构造函数

构造函数

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //注释1 初始化数组
    table = new Entry[INITIAL_CAPACITY];
    //注释2 通过hash与INITIAL_CAPACITY取模(对一个2的整次幂的减一数进行&操作相当于取模)获取firstKey在数组的位置i
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //注释3 插入数据
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    注释4 设置扩容阈值
    setThreshold(INITIAL_CAPACITY);
}
复制代码

通过构造函数底层的存储结构是个数组,通过对 ThreadLocal 的hash值与默认容量取模获取到在数组的位置,插入、更新、删除、数据,并且在必要的时候进行扩容

Entry

看下Entry这个类

    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
复制代码

可以看到Entry继承自WeakReference<ThreadLocal<?>>,是个弱引用,也就是再内存不足的时候,触发GC会被回收

插入数据

从代码片段4我们知道,ThreadLocal#set()函数,会调用到代码片段4注释4出把自身当做key,传入到ThreadLocalMap#set(),如下:

代码片段10
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
        //注释1 从当前位置往后插桩适合插入的位置
    for (Entry e = tab[i];
         e != null;
         //注释2 nextIndex到达尾部之后,会回到开头
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
        //注释3 数组中已经存在,直接更新返回
            e.value = value;
            return;
        }
        //注释4 Entry不为空,key已经被GC
        if (k == null) {
        // 注释5 把脏数据替换为新值
            replaceStaleEntry(key, value, i);
            return;
        }
    }
        //注释6 找到一个空的位置,插入数据
    tab[i] = new Entry(key, value);
    // 注释7size 加1
    int sz = ++size;
    //清理脏数据,判断是否是需要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
复制代码

以上代码可知,ThreadLocalMap#set()的关键是找到合适的位置,首先如果数组中通过key的hash与数组取模得到的索引i,如果i这个位置是空的,那么直接加入,否则循环查找如果找到存在key值一样直接更新,或者找到一个脏数据那就直接替换,否则就找最靠近i的位置,进行插入: 流程图如下:

image.png

读取数据

代码片段11
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
复制代码

逻辑很简单,不过多解释

小结

通过以上对ThreadLocalMap的源码分析,相信对ThreadLocalMap有了初步了解,其实它的底层数据结构就是一个数组,通过开放地址法解决hash冲突。

总结

考虑到本文行文已经过长,决定对ThreadLocalMap的删除数据、扩容缩容以及清除脏数据等细节实现,单独开篇,敬请期待

おすすめ

転載: juejin.im/post/7050799775611879431