关于ThreadLocal与ThreadLocalMap

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zy1994hyq/article/details/83310049

ThreadLocal的一些说明:

 ThreadLocal是一个泛型类,用于在线程中定义局部变量。Thread对象里面有一个默认修饰符修饰的ThreadLocalMap对象变量(ThreadLocalMap是ThreadLocal类里面一个默认修饰符修饰(本类和同包的类可以访问)的内部类)。

ThreadLocal本身并不存储值,它只是在线程的ThreadLocalMap变量中作为一个key来让线程从ThreadLocalMap获取value,每个value都是与ThreadLocal关联在一起的。ThreadLocal存储值,是通过Thread.currentThread()先获取到当前线程对象。由于Thread里面的ThreadLocalMap对象是默认修饰符修饰,而ThreadLocal与Thread位于相同的包中,所以在ThrealLocal类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过Thread对象可以访问到该线程的ThreadLocalMap对象。通过ThreadLocalMap的set方法,key为当前当前ThreadLocal对象,value为想设置的值,说得直白一点就是,ThreadLocal存储值,是通过线程自己内部的ThreadLocalMap存储的。而不同的线程自然就有不同的ThreadLocalMap对象变量。如果在同一个线程中声明了两个ThreadLocal对象,而在Thread内部都是使用仅有那个ThreadLocalMap存放数据的,ThreadLocalMap的key就是ThreadLocal对象,value就是ThreadLocal对象调用set方法设置的值。

通过上面的解释可以看出,ThreadLocal的get方法获取值,其实就是通过获取到当前线程的对象,然后通过当前线程获取到该线程的ThreadLocalMap对象,最后通过ThreadLocalMap拿到最开始设置的值。

ThreadLocal的set方法源码解析:

    //ThreadLocalMap是ThreadLocal的一个默认访问修饰符修饰的内部类,也就是说ThreadLocalMap可以在ThreadLocal内部和同一个包的其他类中使用,
    public void set(T value) {
      //Thread内部定义了一个默认访问修饰符修饰的ThreadLocalMap变量,通过getMap方法可以获取到线程里的ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果map不为空
            map.set(this, value);
        else
            //如果map为空
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        //由于Thread与ThreadLocal位于相同包,所以Thread里可以直接访问到线程里面默认访问修饰符修饰的ThreadLocalMap变量
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        //将当前ThreadLocal对象作为key,新生成一个ThreadLocalMap对象,并将线程的threadLocals对象设置为这个新生成的ThreadLocalMap
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

内部类ThreadLocalMap的一些说明:

ThreadLocalMap内部定义了弱引用的Entry用来存放key和value,定义了Entry数组存放Entry,数组的默认容量为16,并且数组的容量必须为2的幂,比如32,64,128等等。

1.内存泄露

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

2.为什么用弱引用?

key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。ThreadLocalMap在调用set、get、remove的时候会清除Entry数组中所有key为空的Entry,这样就避免了因为value没被回收而造成内存溢出。

由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。  此段引用来自博客https://yq.aliyun.com/articles/574744

ThreadLocalMap声明的一些变量:

        //ThreadLocalMap内部声明的Entry,用于存放key和value,key为ThreadLocal类型的弱引用
       static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //内部Entry数组的默认初始容量为16,数组的容量必须为2的幂
        //由于ThreadLocalMap内部有很多(容量-1)的与运算(&)计算数组索引,
        //容量为2的幂可以保证(容量-1)转化为二进制后最后一位始终是1,这样可以有效的减少碰撞几率
        private static final int INITIAL_CAPACITY = 16;
        //内部Entry数组
        private Entry[] table;
        //数组中存放的元素数量
        private int size = 0;
        //数组扩容临界点
        private int threshold; 

ThreadLocalMap的set方法源码解析:

    private void set(ThreadLocal<?> key, Object value) {
        //table为ThreadLocalMap的Entry数组
        Entry[] tab = table;
        int len = tab.length;
        //通过ThreadLocal类型的key计算索引位置i,采用线性搜索的方法搜索
        int i = key.threadLocalHashCode & (len-1);
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
             //拿到Entry数组中位置i处的Entry的key
             ThreadLocal<?> k = e.get();
             //如果新增的key与k,相同则表示,当前数组存在相同的key的Entry了,此时只需要更新value
             if (k == key) {
                //更新value为新设置的值
                e.value = value;
                return;
             }
             //根据key计算的索引值,进行线性搜索后找到的第一个Key为空的Entry
             if (k == null) {
                //擦除key为空的Entry,并设置key和value
                replaceStaleEntry(key, value, i);
                return;
             }
         }
        //如果方法没有在上面的方法中return,说明此时位置i的Entry是空的,可以设置key和value
        tab[i] = new Entry(key, value);
        int sz = ++size;
        //cleanSomeSlots方法返回false表示数组中已经不存在key为空需要清除的Entry了,此时数组装满了,而 sz 表示此时数组中元素的数量大于临界值了时,需要调用rehash进行扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

replaceStaleEntry方法说明:

该方法的结果就是,将key和value添加到Entry数组中,并且清除Entry数组中由于所有key为空的Entry。

replaceStaleEntry方法源码解析:

    private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
        //staleSlot是在set方法中通过key计算索引,经过线性搜索,找到的第一个Key为null的Entry的所在位置,即:清除元素的开始位置
        //从staleSlot位置反向搜索,因为Entry数组的设计是环形的,因此反向遍历可以遍历到最后一个Entry为null的位置
        int slotToExpunge = staleSlot;
        for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i,len))
            if (e.get() == null)
                // 用slotToExpunge记录最后一个key为null的索引位置
                slotToExpunge = i;

        //从staleSolt向后搜索,直到遇见空的Entry为止
        for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i,len)) {
            ThreadLocal<?> k = e.get();
            //如果k与key相等,则将e的value设置为传入的value
            if (k == key) {
                e.value = value;
                // 将i位置和staleSlot位置的元素对换,如此以来开始遍历的位置就是i位置了,减少了需要遍历的元素,提高遍历效率(staleSlot位置是要清除的元素)
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;
                //如果slotToExpunge与staleSlot相等,则staleSlot位置表示的第一个key为null的Entry也是slotToExpunge表示的最后一个key为null的Entry,即表示数组中只有一个key为null的Entry,上面已经将staleSlot位置的Entry放到了i位置,则此时清除的开始位置slotToExpunge应该为i
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                //从slotToExpunge位置开始清除key为空的Entry
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }
            //如果key为空,而根据slotToExpunge与staleSlot相等可以知道数组中只有一个key为空的Entry,所以此时开始清除的位置slotToExpunge就是当前遍历到的位置i
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 上面的遍历没有遇见空的Entry,则将staleSlot位置的value设为空,并且在此位置放入新的Entry对象
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // 如果slotToExpunge不等于staleSlot表示,第一个key为空的Entry和最后一个key为空的Entry 不是同一个,也就是说Entry数组中存在多个Entry中key为空的对象,则从slotToExpunge位置开始清除
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

expungeStaleEntry方法说明:

方法expungeStaleEntry执行的结果是,从 staleSlot位置开始到最后i位置之间的所有key为空的Entry都被清空。最后返回的位置i 是从staleSlot位置开始向后线性搜索到的第一个Entry为空的位置。

源码解析:

    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        //将Entry数组中staleSlot位置的元素设为空,并且该位置Entry的value也设置为空,总数size减一
        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)) {
            //从staleSlot位置处向后遍历每个位置的Entry,只要Entry里key为空,则删除该位置的元素,将该位置的Entry以及Entry里的value设置为null
            ThreadLocal<?> k = e.get();
            if (k == null) {//Entry里的key为空,则删除此位置的数据
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                //如果Entry里的key不为空,则再次计算索引位置,如果通过key计算的索引位置h与当前索引位置i不相同,则需要将Entry放在数组的新位置中
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    //将此时i位置设为空
                    tab[i] = null;
                    //线性搜索数组中空的位置,将e放入到该位置,
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        //执行到最后可以得出,从staleSlot位置到i位置之间所有key为空的Entry都被清除了
        return i;
    }

cleanSomeSlots方法源码解析: 

该方法可以清除Entry数组中所有key为空的Entry

    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            //从i位置向后遍历
            i = nextIndex(i, len);
            Entry e = tab[i];
            //找到一次key为空的Entry,就执行一次连续清理
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                //执行一次连续段清理
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }

 

展开阅读全文

没有更多推荐了,返回首页