ThreadLocal 原理解析

Posted lxKLj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal 原理解析相关的知识,希望对你有一定的参考价值。

ThreadLocal 核心方法 set 和 get

ThreadLocal 核心对外方法就是set 和 get,通过set 存值,get 取值。

/**
 * 存值
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程中的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // ThreadLocalMap 已经实例化,直接存值
        map.set(this, value);
    else
        // 实例化ThreadLocalMap
        createMap(t, value);
}

/**
 * 取值
 */
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程中的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // ThreadLocalMap 已经实例化,直接取值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 类型强转
            T result = (T)e.value;
            return result;
        }
    }
    // 实例化ThreadLocalMap,并返回初始化值(ThreadLocal 默认null)
    return setInitialValue();
}

从上面两个方法不难看出,ThreadLocal 存取值得空间就是ThreadLocalMap,而通过追踪getMap 我们可以发现,该属性是Thread 类的成员变量,也就是说每个线程都会自带ThreadLocalMap,这也就是各线程ThreadLocal 中的值能够线程隔离的根本原因。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal 中的set 和get 方法在调用时,如果发现ThreadLocalMap 是空,都会进行实例化操作,代码如下所示

/**
 * 创建ThreadLocalMap
 */
void createMap(Thread t, T firstValue) {
    // 构造函数实例化ThreadLocalMap
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

/**
 * table 初始大小
 */
private static final int INITIAL_CAPACITY = 16;

/**
 * ThreadLocalMap 构造函数
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 核心为Entry 类型的数组
    table = new Entry[INITIAL_CAPACITY];
    // 计算存储的数组位置,同HashMap 相似,通过位运算加快计算速度
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 构造Entry 对象存入数组中
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap 实例化完成后,就是往里面存值的操作,通过构造Entry 对象来存入ThreadLocalMap 中的table 数组中。

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)]) {
        // tab[i] 有Entry 对象
        ThreadLocal<?> k = e.get();
        // 相同的ThreadLocal 对象,值覆盖
        if (k == key) {
            e.value = value;
            return;
        }
        // 原ThreadLocal 对象已经被回收,但由于Entry 是弱引用
        // Entry 的key会被置为null,但是value 依然还在
        // 这也是ThreadLocal 可能会导致内存泄露的原因
        if (k == null) {
            // 替换因ThreadLocal已被回收而废弃的Entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // tab[i] 没有对象
    // 创建Entry 插入
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 扩容操作
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

理解set 操作之后,对于get 操作则很容易理解。如下所示,先计算出数组下标后,直接从table 数组中取值即可。

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

总结

通读ThreadLocal 的set 和get 方法后,我们就会发现,其实整个主要就涉及Thread 和ThreadLocal 两个大类。而ThreadLocal 中存取值的容器ThreadLocalMap 却是Thread 的成员变量,是线程唯一的,这就确保了每个Thread 线程都有自己的ThreadLocalMap 容器,相互之间是独立的,这就是ThreadLocal 存储数据线程隔离的根本原因。但是ThreadLocal 对象并不是线程唯一的,我们可以实例化多个ThreadLocal 对象来进行不同值的存储,但是要记得在用完后进行remove 操作。因为ThreadLocalMap 作为Thread 的成员变量,其生命周期是和Thread 相同的,如果Thread 线程没有回收,但是ThreadLocal 却回收了,就会导致ThreadLocalMap 中会存在很多key 为null 的Entry 对象,并且其中存储的值也不会回收,如果而ThreadLocal 对象多的话,可能会导致内存泄漏。特别是当Thread 是在线程中的时候,Thread 不会进行回收操作,ThreadLocal 不进行remove 而导致内存泄露的概率更加的大。

以上是关于ThreadLocal 原理解析的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal 原理解析(并不能解决多线程共享数据安全问题)

ThreadLocal应用和原理解析

ThreadLocal 原理解析

面试必备:ThreadLocal原理解析[精品长文]

ThreadLocal 类 的源码解析以及使用原理

ThreadLocal 类的源码解析