并发编程之ThreadLocal 解读

28,787 阅读4分钟

ThreadLocal实例的弱引用对象会作为key存放在ThreadLocalMap中,然后set方法加入的值就作为ThreadLocalMap中的value。它提供了线程本地变量,可以保证访问到的变量属于当前线程。

属性

private final int threadLocalHashCode = nextHashCode();

//用于计算threadLocal的hash值,每个对象一直递增
private static AtomicInteger nextHashCode =
    new AtomicInteger();

// 黄金分割数 使散列更加均匀
private static final int HASH_INCREMENT = 0x61c88647;

//通过cas向上累加
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

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
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
//初始化一个threadLocalMap
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

通过Thread.currentThread()方法获取了当前的线程引用,通过getMap直接返回Thread实例的成员变量threadLocals。每个线程都有一个ThreadLocal.ThreadLocalMap与ThreadLocal相绑定。

ThreadLocalMap

除了上述属性外,还有一个重要的属性 ThreadLocalMap,ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,源码如下:

static class ThreadLocalMap {
    //键值对的存储结构  继承自弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        // threadLocal为key 被包装成弱引用
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0
}

ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值。

set

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //计算index
    int i = key.threadLocalHashCode & (len-1);
    //获取对应index的entry 如果不为空
    for (Entry e = tab[i];
         e != null;
         //采用线性探测解决哈希冲突
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //不为空并且key相等 则替换
        if (k == key) {
            e.value = value;
            return;
        }
        // key为空说明gc掉了 则替换掉改entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //如果为空 则直接新增entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //进行启发式的垃圾清理,用于清理无用的Entry
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

expungeStaleEntry

private int expungeStaleEntry(int staleSlot) {
    // 1. 将当前的脏entry 置为null,value 置为 null, size,即entry 的数量 减一
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    // 依次循环的使index往后移,直到找到一个 entry = null ,则退出,并返回这个 index
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 在这个过程中,发现脏entry就清除掉,设置为null ,方便GC
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //这里主要的作用是由于采用了开放地址法,所以删除的元素是多个冲突元素中的一个,需要对后面的元素作
            //处理,可以简单理解就是让后面的元素往前面移动
            //为什么要这样做呢?主要是开放地址寻找元素的时候,遇到null 就停止寻找了,你前面k==null
            //的时候已经设置entry为null了,不移动的话,那么后面的元素就永远访问不了了,下面会画图进行解释说明
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现 get 和set 方法都可能触发清理方法expungeStaleEntry(),所以正常情况下是不会有内存溢出的 但是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再使用的时候调用remove(),加快垃圾回收,避免内存溢出。

ThreadLocal 内存泄漏

ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,所以在使用完 ThreadLocal 变量后,需要我们手动 remove 掉。