threadlocal与弱引用

threadLocalMap

1. 内部结构
内部使用threadLocalMap存储线程私有变量,其中threadLocal作为key,用户存储数据作为value

    //... ThreadLocal类内部
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

2. 生命周期
threadLocalMap和线程绑定
1)threadLocal的get、set等方法,都是先获取当前线程,然后通过当前线程获取threadLocalMap。
2)而threadLocalMap其实是Thread类的一个成员变量,它和线程的生命周期相同。因此每次调用threadLocal.set、get、remove方法,都是获取了这个threadLocalMap,其中threadLocal作key,用户数据作为value

//... Thread类内部
ThreadLocal.ThreadLocalMap threadLocals = null;

3. 索引计算
每次ThreadLocal t = new ThreadLocal(),并调用set(),都会在这个map里添加一个threadLocal类型的key。
1)threadLocal的hash值对(entry长度-1)取模,得到索引。
int i = key.threadLocalHashCode & (table.length - 1);
2)获取entry数组中的此元素,如果不为空且key相等,返回该元素

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

3) 否则用线性探测法寻找元素。遍历过程中清除null值的key,避免内存泄漏。

        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;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

4)在set()方法时,如果遍历到entry的key为null,会替换这个entry。

        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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    // 替换
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

4. 扩容
1)数组的初始容量是16。
2)如果在set()方法最后,遍历部分数组但没有清理掉任何数据时,且数组中entry数量 >= 数组长度的2/3(threshold容量),执行rehash()方法

int sz = ++size;
// 从i开始查找key为null的非空entry(脏entry),找到后清理并继续往后,直到空entry
if (!cleanSomeSlots(i, sz) && sz >= threshold)
     rehash();

rehash()方法方法中,清理key为null的entry,此时如果 size >= threshold*2/3,扩容原来的2倍容量

        private void rehash() {
            // 清理null值key的entry
            expungeStaleEntries();

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

哈希冲突解决方案

  • 线性探测法
    出现hash冲突时(计算出索引冲突),继续探测下一个索引槽位,直到探测到可以插入为止。threadLocalMap索引冲突解决方式
  • 再hash法
    构造多个hash函数,发生冲突时,更换hash函数并计算,直到不冲突
  • 链地址法
    将hash冲突的节点生成一个链表。例如hashmap
  • 建立公共溢出区
    将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

弱引用

threadlocalMap的enrty对象继承了WeakReference,这个enrty对象持有对threadlocal的弱引用。

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

对象只被弱引用所引用,在下一次gc时该对象就会被回收。

内存泄漏

1)threadLocal的强引用丢失,例如手动把引用设置为null、或者这个强引用在局部方法里(前两者会有人这么做吗)。这样就会只剩下弱引用导致threadLocal对象被回收,entry对象的key变为了null,导致value无法被访问。但这种情况下似乎很少会出现。
2)在调用get、set方法时,因为线性探测法的原因进行遍历,会清除key为null的entry,并不能保证清除所有的key为null的entry。resize()扩容的时候会清楚所有脏entry。
3)或者说,线程池中的线程的生命足够长,某一段代码在向threadLocal设置value后,没有remove()就结束并重新将线程放回线程池,导致内存泄漏。就是在说线程没有即使结束,然后回收掉threadLocalMap,导致内存的泄漏。

posted @ 2023-02-28 11:28  OraCat  阅读(45)  评论(0编辑  收藏  举报