前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThreadLocal全面解析

ThreadLocal全面解析

作者头像
虞大大
发布2020-08-26 17:18:33
4210
发布2020-08-26 17:18:33
举报
文章被收录于专栏:码云大作战

一、ThreadLocal分析

ThreadLocal类保证了线程内部的变量在多线程环境下相对于其他线程是不可见的。

· ThreadLocal数据结构

上述图片为threadLocal的数据结构,每一个线程都维护一个threadLocalMap,key为线程中子线程构造的threadLocal。线程中对threadLocal的set、get、remove操作其实就是对threadLocalMap的操作。

· ThreadLocal内部属性

代码语言:javascript
复制
//hash值,底层调用了nextHashCode方法,即每次新增一个threadLocal//就会使原来的hash值加上HASH_INCREMENTprivate final int threadLocalHashCode = nextHashCode();
//原子操作
private static AtomicInteger nextHashCode =
        new AtomicInteger();
//这是一个黄金分割值,让一个原子类不断加这个数,目的是减少hash冲突
private static final int HASH_INCREMENT = 0x61c88647;
//cas 当前hash+0x61c88647
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

内部属性和我们认知中的HashMap还是有所不同的,hashMap中key的hash值的确定是根据key的高低位进行与运算求出来的,而ThreadLocaMap中的hash值是不断累加HASH_INCREMENT这个数而求得hash值。主要原因还是为了解决Hash冲突,而threadLocalMap中采用的方法是数组探测法。

· ThreadLocal常用方法

get方法

代码语言:javascript
复制
//获取当前线程的值
public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取threadLocalMap对象
    ThreadLocalMap map = getMap(t);
    //map不为空
    if (map != null) {
        //去map中取出存储信息
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            //线程存放在map中的值
            T result = (T) e.value;
            return result;
        }
    }
    //map为空,表明,当前线程没有threadMap对象,需要初始化
    return setInitialValue();
}//初始化线程的threadLocalMap
private T setInitialValue() {
    //获取初始化的值
    T value = initialValue();
    Thread t = Thread.currentThread();
    //再次获取map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map存在,则设置entry key-threadLocal对象 value-值
        map.set(this, value);
    else
        //否则创建map,当前线程entry为第一个结点
        createMap(t, value);
    //返回设置的值
    return value;
}

set方法

代码语言:javascript
复制
//设置值
public void set(T value) {
    //当前线程
    Thread t = Thread.currentThread();
    //获取map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map如果存在,则直接set
        map.set(this, value);
    else
        //map不存在,则直接创建,当前线程entry为第一个结点
        createMap(t, value);
}

remove方法

代码语言:javascript
复制
//移除操作
public void remove() {
    //获取当前线程的threadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        //移除
        m.remove(this);
}

上述方法的过程都比较简单,但是对threadLocal的操作实际上是在对threadLocalMap进行操作,所以我们需要搞懂ThreadLocalMap即可。

· ThreadLocalMap内部类

代码语言:javascript
复制
//Entry为弱引用
static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
    //value值
    Object value;
    //key为threadLocal
    Entry(java.lang.ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

可以看出threadLocalMap中的entry为弱引用,即当内存空间不足发生gc时,会把弱引用回收掉。

代码语言:javascript
复制
//初始容量
private static final int INITIAL_CAPACITY = 16;
//table数组
private Entry[] table;
//元素个数
private int size = 0;
//扩容临界值
private int threshold;
//临界值 = table长度 * 2/3
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
代码语言:javascript
复制
//下一个数组下标
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

//上一个数组下标
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

· ThreadLocalMap构造函数

代码语言:javascript
复制
//构造函数 指定key和value
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //使用默认初始容量16
    table = new Entry[INITIAL_CAPACITY];
    //第一个key为0x61c88647 & 15
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //设置临界值
    setThreshold(INITIAL_CAPACITY);
}

指定key和value的构造函数主要被用于threadLocal的set方法中。该构造函数主要做了这些事情,构建了一个默认初始容量16的数组,根据key的hash值和数组下标最大值进行与运算求出数组下标(因为刚构造出来,所以这里的hash值肯定为0,后续如果有新的key那么hash值为0+0x61c88647),将key和value构建成entry放到数组第一个结点中,最后设置size值和扩容临界值。

代码语言:javascript
复制
//指定map的构造函数
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    //根据指定的数组大小 设置临界值
    setThreshold(len);
    //构建一个新的map
    table = new Entry[len];
    //循环放入值到map中
    for (int j = 0; j < len; j++) {
        //指定map中的entry
        Entry e = parentTable[j];
        if (e != null) {
            //获取key
            java.lang.ThreadLocal<Object> key = (java.lang.ThreadLocal<Object>) e.get();
            if (key != null) {
                //计算子线程的value
                Object value = key.childValue(e.value);
                //根据指定entry中的数据构建新的entry
                Entry c = new Entry(key, value);
                //根据hash值和数组下标最大值 求出下标
                int h = key.threadLocalHashCode & (len - 1);
                //如果当前table中存在了entry,则放到table[h+1]中
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

上述为指定map的构造函数,主要过程为需要依次将指定map中的元素放入到新构建出来的数组中。放置的顺序为数组形式放置。

· ThreadLocal的get方法底层实现原理

代码语言:javascript
复制
//获取当前线程的值
public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取threadLocalMap对象
    ThreadLocalMap map = getMap(t);
    //map不为空
    if (map != null) {
        //去map中取出存储信息
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            //线程存放在map中的值
            T result = (T) e.value;
            return result;
        }
    }
    //map为空,表明,当前线程没有threadMap对象,需要初始化
    return setInitialValue();
}
代码语言:javascript
复制
//根据key获取entry
private Entry getEntry(java.lang.ThreadLocal<?> key) {
    //求出下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //key存在 则直接返回
    if (e != null && e.get() == key)
        return e;
    else
        //不存在,继续查找
        return getEntryAfterMiss(key, i, e);
}
代码语言:javascript
复制
private Entry getEntryAfterMiss(java.lang.ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        //获取entry 的key
        java.lang.ThreadLocal<?> k = e.get();
        //key一致,直接返回
        if (k == key)
            return e;
        //key为空,处理过期的数据,因为弱引用,需要删除已经为null的引用
        if (k == null)
            expungeStaleEntry(i);
        else
            //获取下一个数组下标
            i = nextIndex(i, len);
        //下一个数组下标对应的entry
        e = tab[i];
    }
    return null;
}
代码语言:javascript
复制
//删除对应位置过期数据
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    //删除指定位置上的数据
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
    Entry e;
    int i;
    //循环当前位置——>数组最大位置
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        java.lang.ThreadLocal<?> k = e.get();
        //若当前位置引用为空,则全部置为null
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //计算 下标
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                //根据重新计算的下标继续遍历到最后
                //因为还可能存在多个过期的实体
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}//初始化线程的threadLocalMap
private T setInitialValue() {
    //获取初始化的值
    T value = initialValue();
    Thread t = Thread.currentThread();
    //再次获取map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map存在,则设置entry key-threadLocal对象 value-值
        map.set(this, value);
    else
        //否则创建map,当前线程entry为第一个结点
        createMap(t, value);
    //返回设置的值
    return value;
}

get方法主要为以下几个步骤。

(1)获取当前线程的ThreadId,根据threadId获取threadLocalMap。

(2)threadLocalMap不存在,则进行初始化操作,即创建threadLocalMap。

(3)threadLocalMap若存在,则进行查询entry操作。

(4)求出数组下标,获取数组下标对应的entry,若entry不为空并且key为当前的threadLocal对象,则为需要查询的数据,直接返回。

(5)若不存在,则还需要继续往下一个数组中进行查找。

(6)遍历数组,如果查询到key并且一直则直接返回。

(7)否则需要判断当前遍历到的key是否为null,如果为空,说明该弱引用被回收,需要删除已经为null的引用。

(8)key不为空,进行下一次循环查找。

(9)直到循环结束或者查找到对应key则返回。

(10)这里的清数据操作会清楚所有key被回收掉了的数据。

· ThreadLocal的set方法底层实现原理

代码语言:javascript
复制
public void set(T value) {
    //当前线程
    Thread t = Thread.currentThread();
    //获取map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map如果存在,则直接set
        map.set(this, value);
    else
        //map不存在,则直接创建,当前线程entry为第一个结点
        createMap(t, value);
}
代码语言:javascript
复制
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)]) {
        java.lang.ThreadLocal<?> k = e.get();
        //如果存在当前的key,则直接返回
        if (k == key) {
            e.value = value;
            return;
        }

        //如果当前位置无key
        if (k == null) {
            //存放新值
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //map中 当位置key不相同并且不为null 直接放置到当前位置中
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //清理数据
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        //扩容 - 如果没有要清理的数据并且容量超过了临界值
        rehash();
}
代码语言:javascript
复制
private void replaceStaleEntry(java.lang.ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    //往前找 找到第一个已经被清理的下标
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    //往后找进行遍历
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        java.lang.ThreadLocal<?> k = e.get();
        //如果当前遍历的key是要插入的key
        if (k == key) {
            //进行替换
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            //清理过期的数据
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        //如果没有往后找到过期实例
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    //key没有找到,设置新的entry实例
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    //清除过期实例
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

set方法主要为以下几个步骤:

(1)获取当前线程threadId,获取threadLocalMap,如果map不存在,则创建map。map中的entry就为需要set的值。

(2)map如果存在调用threadLocalMap的set方法。

(3)在set方法中会先根据hash值和数组大小计算下标。

(4)从下标开始进行循环,如果循环过程中存在当前的key则直接返回。

(5)如果map数组下标中对应的key是null,则调用replaceStaleEntry存放新值。

(6)replaceStaleEntry中的操作,主要为清楚过期数据和设置entry值。

(7)如果循环查找不存在key,并且位置不为null,则直接将需要set的值放入到计算的下标中。

(8)清理过去数据,判断是否需要扩容。

(9)扩容操作。

扩容操作如下:

代码语言:javascript
复制
//扩容 1、还会删除过期entry实例 2、进行扩容
private void rehash() {
    expungeStaleEntries();
    //当前容量 >= 阈值的3/4
    if (size >= threshold - threshold / 4)
        resize();
}
代码语言:javascript
复制
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //新容量为原来的2倍
    int newLen = oldLen * 2;
    //构建新的数组
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    //对老的数组进行遍历 、 复制到新数组中
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            java.lang.ThreadLocal<?> k = e.get();
            if (k == null) {
                //如果key为null,value也置为null,帮助gc
                e.value = null;
            } else {
                //重新计算hash值
                int h = k.threadLocalHashCode & (newLen - 1);
                //放在数组中下一个空闲位置
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    //重新设置临界值
    setThreshold(newLen);
    size = count;
    table = newTab;
}

· ThreadLocal的remove方法底层实现原理

代码语言:javascript
复制
//移除操作
public void remove() {
    //获取当前线程的threadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        //移除
        m.remove(this);
}
代码语言:javascript
复制
代码语言:javascript
复制
//threadLocalMap - 根据key移除
private void remove(java.lang.ThreadLocal<?> key) {
    //map中的数组
    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;
        }
    }
}

remove操作比较简单,主要分为以下几个步骤:

(1)根据key的hash值和数组下标计算key存放的下标。

(2)循环数组,如果key存在数组中则进行清除操作。

二、ThreadLocal总结

ThreadLocal类内部维护了一个threadLocalMap,该map和hashMap一样可以进行简单的set、get、remove、扩容等基本操作,不过hashMap对于hash冲突采用的是拉链法+红黑树,而threadLocalMap中采用的是线性探测法。并且threadLocalMap中的key为子线程构造出来的threadLocal对象,是一个弱引用,因此会在一定时机被gc回收,因此在对threadLocal类进行操作的时候内部会有清楚过期entry的操作。

· ThreadLocal内部的引用关系。

内部中存在thread—>threadLocalMap—>entry—>threadLocal强引用关系,而entry中的key是弱引用,因此即使发生了垃圾回收,key会被置为null,但是entry中存在强引用关系,无法被回收。久而久之容易造成内存泄漏。

虽然在set、get操作时会进行清理过期数据的操作,但是尽量还是保证在使用threadLocal类后进行remove操作,减少内存泄漏的风险 —— 及时清理不必要的值。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码云大作战 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档