ThreadLocal源码解析(二)

本文深入剖析ThreadLocalMap内部结构及工作原理,包括其Entry类、数据存储方式、扩容及键值对查找机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇文章主要讲述了Thread和ThreadLocal的关系以及ThreadLocal是怎么存储数据的,本文将讲述ThreadLocal中的一个内部类static class ThreadLocalMap,这个类与HashMap类似,但是跟HashMap没有任何关系,甚至与Map都没有关系,我们主要通过源码来看看该类是什么实现这个功能的。

1.ThreadLocalMap中有一个内部类Entry :
Entry存储键值对,键为ThreadLocal,值为要存储的值。这个类继承了WeakReference类,表示ThreadLocal是一个弱引用型的对象。至于为什么是弱引用型的,会带来什么问题,请看后续讲解。

弱引用型:当垃圾回收期扫描他所管辖的区域是,一旦发现这个弱引用型的变量,不论内存是否够用,都会回收。

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

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

2.数据结构
上面介绍了Entry,没错,在ThreadLocalMap中就是靠Entry来存储数据的,内部用一个Entry的数组来存放数据,所以说ThreadLocalMap中绝大部分的操作都是针对这个Entry数组的。

private Entry[] table;

这个table数组就是一张Hash表,这样就会涉及好加载因子,以及扩容,我们依次介绍这大问题:

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

/**
 * Set the resize threshold to maintain at worst a 2/3 load factor.
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

从源代码可以看出。加载因子设置为2/3,当键值对个数超过总长度的2/3时,就应该扩容。设置加载因子主要为了快速查询。

3.存储键值对
将键值对放入到hash表(数组)中:

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;

//计算hash值   
 int i = key.threadLocalHashCode & (len-1);

//从i位置开始找到一个e
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

//如果k在表中出现过,则覆盖
        if (k == key) {
            e.value = value;
            return;
        }
//如果k没有出现过,就直接赋值
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

//如果该位置已经出现元素,则将元素向后移动,后面的依次类推,移动时判断闸值,超过闸值,则扩容。
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

总结下过程:
通过key(ThreadLocal类型)的hashcode来计算存储的索引位置i。如果i位置已经存储了对象,那么就往后挪一个位置依次类推,直到找到空的位置,再将对象存放。另外,在最后还需要判断一下当前的存储的对象个数是否已经超出了阈值(threshold的值)大小,如果超出了,需要重新扩充并将所有的对象重新计算位置(rehash函数来实现)。

rehash()函数的实现:

private void rehash() {
    expungeStaleEntries();

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

rehash函数里面先调用了expungeStaleEntries函数,然后再判断当前存储对象的大小是否超出了阈值的3/4。如果超出了,再扩容。为什么要先调用expungeStaleEntries()呢?
前面我们讲到Entry中存的键是一个弱引用的ThreadLocal,该对象随时可能被gc垃圾回收机制回收,导致value值对应的key为null,这样一来就需要把这样的键值对给回收了,不让他们占地方,expungeStaleEntries就是做这样一个清理工作,清理完成后,实际的键值对会减少,这样一来我们的闸值变为3/4也得到了解释。

private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                //Entry对象不为null,但键值对的值为null,
                则调用expungeStaleEntry清除
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }
//staleSlot对应的数组元素的键为0,需要将该元素清除掉
 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //值设为null
            tab[staleSlot].value = null;
            //该数组位置设置为null
            tab[staleSlot] = null;
            size--;//数组中元素数量减1.

            // Rehash until we encounter null
            //继续重复,直到我们遇到空
            Entry e;
            int i;
            /*
            初始条件:i = nextIndex(staleSlot, len):下一个下标
            条件:(e = tab[i]) != null:该值不为空
            步长:i = nextIndex(i, len)下一个下标
            */
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                //key如果为空,就回收
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                //key不为空,就再hash计算
                    int h = k.threadLocalHashCode & (len - 1);
                    //计算的值不为j,说明前面有元素回收过了
                    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);
                        //把e的值放在h的位置
                        tab[h] = e;
                    }
                }
            }
            return i;//返回的是i为空的位置
        }

总结该方法是如何清理的:
把ThreadLocalMap里面的key值遍历一遍,为null的直接删了即可。可是,前面我们说过,ThreadLocalMap并没有实现Java.util.Map接口,即无法得到keySet。其实,不难发现,如果Key值为null,此时调用ThreadLocalMap的get(ThreadLocal)相当于get(null),get(null)返回的是null,这也就很好的解决了判断问题。也就是说,无需判断,直接根据get函数的返回值是不是null来判定需不需要将该Entry删除掉。注意,因为get()返回的是键,所以get返回null也有可能是key的值为null,但是对于get返回为null的Entry,也没有占坑的必要,同样需要删掉,这么一来,就一举两得了。

另一个方法resize()扩容:扩容为原来的两倍,将原数组的值拷贝到新数组中,中间遇到键为null,则直接回收。

private void resize() {
            Entry[] oldTab = table;//旧数组
            int oldLen = oldTab.length;//旧数组的长度
            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) {
                    ThreadLocal k = e.get();
                    //key为空,弱引用直接回收
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                    //不为空,计算新数组的hash值
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //放到合适的位置,还是遇到冲突,顺序向后放
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        //新数组赋值
                        newTab[h] = e;
                        //新数组的个数+1
                        count++;
                    }
                }
            }

            //设置闸值
            setThreshold(newLen);
            size = count;//实际长度赋值给size
            table = newTab;//新数组拷贝给table。
        }

getEntry()方法:主要是根据键来获取键值对。
这个方法与HashMap不同,因为HashMap的内存结构是数组+链表,根据键去找键值对时,是先计算Hash值,根据Hash值来确定目标节点所在的链,在链上查找。而该类中的数据结构只是一个Entry数组,加入元素时出现冲突了,会顺序向后查找空位置来放这个键值对。所以我们在查找的时候,该位置的key不等于我们给出的key是,需要调用
getEntryAfterMiss(…)方法到下一个位置查找。
源代码:

private Entry getEntry(ThreadLocal key) {
            //计算key的hash值
            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;
                //key值为null,则回收掉!做的好!!
                if (k == null)
                    expungeStaleEntry(i);
                else//否则找下一个位置
                    i = nextIndex(i, len);
                //让e为下一个位置的元素
                e = tab[i];
            }
            //返回null。
            return null;
        }

总结下:
从指定位置开始顺序向下查找,直到找到key与我们提供的k相等时,返回,查到完毕后还是没有找到,则返回null。有一点我觉得做的非常好:在找的过程中遇到key为null的,直接帮助垃圾回收机制回收掉,这样节约空间,加快性能啊。

本文对ThreadLocal就讲述到这里,相信大家对这个类也有了一定的了解,结合上一篇文章,则可以更全面的学习到ThreadLocal类,下篇文章将总结下ThreadLocal的会出现的问题,请关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值