Java ThreadLocal原理分析

372 阅读4分钟

1 简述

ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同,而同一个线程在任何时候访问这个本地变量的结果都是一致的。当此线程结束生命周期时,所有的线程本地实例都会被 GC。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。

2 原理

ThreadLocal是一种数据结构,有点像map,但是一个ThreadLocal只能保存一个。
每个线程有一个 ThreadLocalMap 成员变量,本质是一个 map。map 里存储的 key 是一个弱引用,其包装了当前线程中构造的 ThreadLocal 对象,这意味着,只要 ThreadLocal 对象丢掉了强引用,那么在下次 GC 后,map 中的 ThreadLocal 对象也会被清除,对于那些ThreadLocal 对象为空的 map 元素,会当为垃圾,稍后会被主动清理。map 里存储的 value 就是缓存到当前线程的值,这个 value 没有弱引用去包装,需要专门的释放策略。

3 核心方法

get()

/**
 * 返回当前 ThreadLocal 对象关联的值
 *
 * @return
 */
public T get() {
    // 返回当前 ThreadLocal 所在的线程
    Thread t = Thread.currentThread();
    // 从线程中拿到 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从 map 中拿到 entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果不为空,读取当前 ThreadLocal 中保存的值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }
    // 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,最后返回当前的 ThreadLocal 对象关联的初值,即 value
    return setInitialValue();
}

set()

/**
 * 为当前 ThreadLocal 对象关联 value 值
 *
 * @param value 要存储在此线程的线程副本的值
 */
public void set(T value) {
    // 返回当前 ThreadLocal 所在的线程
    Thread t = Thread.currentThread();
    // 返回当前线程持有的map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
        map.set(this, value);
    } else {
        // 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
        createMap(t, value);
    }
}

remove()

/**
 * 清理当前 ThreadLocal 对象关联的键值对
 */
public void remove() {
    // 返回当前线程持有的 map
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        // 从 map 中清理当前 ThreadLocal 对象关联的键值对
        m.remove(this);
    }
}

可以发现,不管是get、set、remove操作的都是当前线程持有的ThreadLocalMap。
每个线程都有一个自己的ThreadLocalMap。
所以如果线程A set 值到map中,线程B又 set ,两者是不会互相影响的。

4 ThreadLocalMap

最后,我们看看ThreadLocalMap的实现。

/**
 * 键值对实体的存储结构
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    //当前线程关联的 value,这个 value 并没有用弱引用追踪
    Object value;
    /**
     * 构造键值对
     *
     * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
     * @param v v 作 value
     */
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

WeakReference保证GC时能够回收线程set的值。

// 返回 key 关联的键值对实体
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 若 e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回
    if (e != null && e.get() == key) {
        return e;
    } else {
        // 从 i 开始向后遍历找到键值对实体
        return getEntryAfterMiss(key, i, e);
    }
}

// 在 map 中存储键值对<key, value>
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算 key 在数组中的下标
    int i = key.threadLocalHashCode & (len - 1);
    // 遍历一段连续的元素,以查找匹配的 ThreadLocal 对象
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取该哈希值处的ThreadLocal对象
        ThreadLocal<?> k = e.get();
        // 键值ThreadLocal匹配,直接更改map中的value
        if (k == key) {
            e.value = value;
            return;
        }
        // 若 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 直到遇见了空槽也没找到匹配的ThreadLocal对象,那么在此空槽处安排ThreadLocal对象和缓存的value
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 如果没有元素被清理,那么就要检查当前元素数量是否超过了容量阙值(数组大小的三分之二),以便决定是否扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        // 扩容的过程也是对所有的 key 重新哈希的过程
        rehash();
    }
}

从源码中我们可以看到,ThreadLocalMap和HashMap有点相似,但是ThreadLocalMap没有采用数组+链表(红黑树)的方式解决hash冲突,而是采用线性探测寻址法

5 总结

每个Thread都有一个ThreadLocalMap成员变量,这个变量和ThreadLocal绑定在一起,很好的给线程提供了独享数据的地方。
对比多线程资源共享,采用同步机制(以时间换空间),此方式是以空间换时间,性能比较好。
不过使用ThreadLocal的时候,还需要注意内存泄露的问题,虽然是采用WeakReference,但是如果线程一直运行还是可能导致内存泄露,所以最好是使用完后 remove 一下。