从源码看ThreadLocal的底层结构

使用场景

ThreadLocal可以用于多线程情况下的变量保存,各个线程之间的变量值互不影响。关于它的使用场景可以看
刘欣 老师的文章一个故事讲明白线程的私家领地:ThreadLocal,这篇文章通过生动的案列引出了使用场景,令人印象深刻。

使用方法

ThreadLocal<String> threadLocalA= new ThreadLocal<String>();
ThreadLocal<Integer> threadLocalB = new ThreadLocal<Integer>();

//保存
线程1: threadLocalA.set("Sbingo");
        threadLocalB.set(99);
线程2: threadLocalA.set("sbingo");
        threadLocalB.set(100);

//使用
线程1: threadLocalA.get()  --> "Sbingo"
        threadLocalB.get()  --> "99"
线程2: threadLocalA.get() --> "sbingo"
        threadLocalB.get()  --> "100"

可以看到,ThreadLocal的使用方法很简单,为什么这么操作后同一个变量在不同线程中取出的值不一样,并且1个线程可以保存N个ThreadLocal呢?

原理就在Thread类中成员变量threadLocals,它的类型是ThreadLocal.ThreadLocalMap,每个线程都有一个这样的Map,所以可以保存N个ThreadLocal键值对,并且不同线程的变量值不同。

因此,按上面这样操作后相应的数据结构如下:

线程1中map

key value
threadLocalA Sbingo
threadLocalB 99

线程2中map

key value
threadLocalA sbingo
threadLocalB 100

变量threadLocals虽然定义在Thread类中,但对它的访问却完全交给了类ThreadLocal,下面我们通过源码看看ThreadLocal.ThreadLocalMap具体是如何保存这些键值对的。

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);
}

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

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

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);
}

static class Entry extends WeakReference<ThreadLocal<?>> {
     /** The value associated with this ThreadLocal. */
     Object value;

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

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

从第11行可以看出,getMap()方法就是返回了刚才提到的Thread类中成员变量threadLocals

当首次调用set()方法时,会进入第7行的createMap()方法,第15行赋值给提到过的线程变量threadLocals,自此它不再是null

第15行在构造方法中传入了this,也就是将当前的ThreadLocal本身作为了key

进入构造方法,tableMap中的哈希表,第19行对其赋值,初始容量为16。table中保存的元素类型为Entry,它是对ThreadLocal的弱引用(第26行),这样便于GC,防止内存泄漏。Entry中只有一个变量value(第28行),value就是Map中每个ThreadLocal对应的值。

扫描二维码关注公众号,回复: 2707271 查看本文章

第20行将threadLocalHashCodeINITIAL_CAPACITY - 1进行按位与运算,相当于threadLocalHashCodeINITIAL_CAPACITY取模,但速度比用%运算更快,这样就计算出了哈希表中索引的位置。

最后对哈希表相应索引的元素赋值,更新表内元素数量和阙值,完成了首次set

之后再调用set()方法时,就会进入第5行代码。

还是和之前一样计算出索引,当发生冲突时,进入第41至55行的循环体内。

从第64行可以看出,解决冲突的nextIndex()方法用的是线性探测法

第46至49行,如果是同一个ThreadLocal对象,则更新对应的值。

第51至54行,如果ThreadLocal的引用为null,则用当前的新值替换旧值,为简化篇幅,具体源码这里不展开了。

如果没发生冲突、发生冲突但遍历结束也没找到相同的ThreadLocal对象或null对象,则插入新值,此时执行第57行。

第59行表明在插入新值后,尝试删除已回收的值,如果没有删除发生并且哈希表内元素数量超过了阙值,则扩容哈希表并重新排列表内元素。

第76行表明阙值大小为哈希表长度的2/3 ,第71行表明扩容时哈希表内元素数量为阙值的3/4,相乘为哈希表大小的1/2。即当哈希表内元素数量超过哈希表大小一半时,就会发生扩容。扩容后的大小为原大小的两倍,这里也不展开分析了。

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();
}

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;
}

protected T initialValue() {
    return null;
}

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);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

get()发生在set()之前,就会进入第12行代码。此后在第16行调用initialValue()方法返回了null,当然也可以复写该方法以修改默认值。第22行会初始化Map,最后返回了初始值(null或修改后的初始值)。

当然大多数情况下调用get()方法会进入第5行获取Entry对象,在第8、9行获取对应的value值并返回。可见,关键在于getEntry()方法。

第31行还是计算出索引,第33行判断索引处的Entry对象是否为null并比较两个ThreadLocal对象是否为同一个,结果为真的话就可以直接返回Entry对象,否则进入getEntryAfterMiss()方法。

第43至52行的循环体中,不断遍历哈希表,当两个ThreadLocal对象为同一个时,就可以返回哈希表当前索引上的Entry对象。

内存泄漏

ThreadLocal虽然用起来很简单,但使用时要注意内存泄漏问题。

刚才分析时说过,Entry对象是ThreadLocal的弱引用。所以当线程消亡时,垃圾回收器会将它回收,此时不存在内存泄漏问题。

但如果使用线程池,并且线程存在复用的情形,线程不会消亡,这些Entry对象就会发生泄漏,此时就需要我们进行手动清理。

清理的方法很简单,就是调用ThreadLocalremove()方法,和上面的分析类似,它会不停探测索引,如果找到正确的ThreadLocal对象就将其清除。

相关源码如下:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

猜你喜欢

转载自blog.csdn.net/recordGrowth/article/details/79945433