Java多线程深入学习-ThreadLocal源码解析2.深入

1.Entry?弱引用?

上一讲我们简单看了ThreadLocal的入门,了解到ThreadLocal是如何保证填充的变量是属于当前线程的。这里我们继续分析ThreadLocal源码。上一讲我们最后看到了ThreadLocalMap类的定义,发现其中定义了一个内部类Entry继承了WeakReference,代码如下:

        /**
         * Entry类继承WeakReference,这是一个弱引用 
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;	

            /**
             * 这里对ThreadLocal k的引用是若引用  若ThreadLocal没有再被其他有效数据强引用着,则下次垃圾回收的时候会将k对象回收掉
             */
            Entry(ThreadLocal<?> k, Object v) {
                super(k);	//对key是弱引用  即对ThreadLocal是若引用
                value = v;	//对value是强引用
            }
        }

观察其构造方法,接收了两个参数,一个是我们的ThreadLocal对象,另一个是我们要往ThreadLocal中填充的变量Object,Entry类定义了一个Object value的属性,Entry对value的引用是强引用,而对于另一个参数ThreadLocal对象的处理交给了父类WeakReference,这代表Entry对ThreadLocal<?> k的引用是一个弱引用,只被弱引用关联的对象将会在下一次垃圾回收时被回收掉。

2.弱引用+线程池 导致的内存泄漏问题

ThreadLocal对象结合多线程使用,在没有使用线程池的情况下,线程执行完毕会被销毁,那么包含Thread中的ThreadLocalMap,以及ThreadLocalMap中的Entry数组table,以及数组中的Entry对象,包含Entry对象中的value都属于不可达对象,都将会在下一次垃圾回收的时候被回收掉,也就不存在内存泄漏的问题。

可是,当我们使用线程池的时候,核心线程执行完当前任务后并不会被销毁,而是会去执行下一个任务,也就是说ThreadLocalMap会一直被Thread引用着,不会被GC包括ThreadLocalMap中的Entry数组table,以及数组中的Entry对象,包含Entry对象中的value,可是由于Entry对象对ThreadLocal的引用是弱引用,当任务执行完毕,任务会被销毁,对应的ThreadLocal对象也会被清理掉。此时,ThreadLocalMap中的value对应的key就会变成null,我们就无法获取到这个value,这样就会造成内存泄漏的问题。

3.如何避免内存泄漏问题

方案1:提供方法,在ThreadLocal使用完成之后调用方法清除ThreadLocalMap中的value,感觉可行,但不保证别人使用之后一点给会调该方法。

方案2:定时对Thread中的ThreadLocalMap中key为null的value值进行清理,需要搞定时相关东西,麻烦,复杂,Pass掉该方案

方案3:还是要对Thread中的ThreadLocalMap中key为null的value值进行清理,改成触发机制,在进行某些操作的时候触发该机制。 感觉可行,在特殊情况下,某个任务之后若没有再使用到ThreadLocal,那么对于之上还未处理的value,还是存在内存泄漏问题。

总结:相对来说,方案2最为保险,但是实现起来比较复杂,而且会占用更多的资源。方案1和方案3在特殊情况下都存在一定的问题,若是13两个方案一起使用,出现问题的几率就大大的降低了。

豆豆:别给这瞎猜了,去看源码吧。

。。。。。。

一顿秃头式的啃源码之后,发现确实跟我想的一样,是用的13两个方案一起。下面我们一起来看一下

3.1 ThreadLocal中的remove

    /**
     * 移除数据
     */
     public void remove() {
    	 //获取当前线程,并获取当前线程中的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)	//ThreadLocalMap不为null的时候
             m.remove(this);	//调用其remove方法将this(ThreadLocal移除调)
     }

这里调用了ThreadLocalMap的remove方法,我们看一下这个方法

        /**
         * 移除
         */
        private void remove(ThreadLocal<?> key) {
        	//获取Entry数组
            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();	//清除Entry对ThreadLocal key的弱引用
                    expungeStaleEntry(i);	//调用expungeStaleEntry进行处理
                    return;	//返回
                }
            }
        }

这里会调用expungeStaleEntry方法进行处理,直接翻译意思是清除无用的Entry,我们看一下其对应的源码

       /**
         * 清除无用的Entry
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;	//获取Entry数组
            int len = tab.length;	//获取数组长度

            tab[staleSlot].value = null; //将数组中staleSlot位置数据的value置空(取消强引用)
            tab[staleSlot] = null;	//将数组的staleSlot位置的Entry对象置空
            size--;	//存储数据数-1

            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;	//遍历条件tab[i] != null
                 i = nextIndex(i, len)) {	//从当前位置的下一个节点开始向后遍历
                ThreadLocal<?> k = e.get();	//获取Entry对象对应的k  也就是ThreadLocal对象
                if (k == null) {	//判断对应的k为null 说明对应的ThreadLocal对象已被GC
                    e.value = null;	//将对应value置空
                    tab[i] = null;	//将数组中对应位置置空
                    size--;	//存储数据-1
                } else {	//k 不为空的情况处理	即k还未被GC
                    int h = k.threadLocalHashCode & (len - 1);	//根据k 计算下标
                    if (h != i) {	//下标不为i  即无法通过该k获取到i下标的数据  
                        tab[i] = null;	//直接清除数组中对应下标位置

                        while (tab[h] != null)//从数组中 k对应下标h位置开始遍历,找到第一个Entry为空的位置 h
                            h = nextIndex(h, len);
                        tab[h] = e;	//将k对应节点信息直接放到数组中下标为h的位置
                    }
                }
            }
            return i; //返回结果
        }

这里发现会清理掉没用的数据,并调整后面数据的位置,以上就是ThreadLocal中remove方法处理的全过程。

3.2 ThreadLocal中的get方法

这里的get方法也会处理ThreadLocalMap中的无效数据的,我们看一下它是如何进行的

    /**
     * 获取数据
     */
    public T get() {
        //获取当前线程
    	Thread t = Thread.currentThread();
        //获取对应ThreadLocalMap
    	ThreadLocalMap map = getMap(t);
        if (map != null) {	//map不为null
        	//根据当前ThreadLocal对象获取map中对应的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {	//若节点e存在
                @SuppressWarnings("unchecked")
                T result = (T)e.value;	//获取节点中对应的value
                return result;	//返回value
            }
        }
        //map为null或者map中未获取到对应的Entry  调用setInitialValue方法
        return setInitialValue();
    }

我们先分析setInitialValue方法,这个方法会在map == null或者map中未获取到对应的Entry时调用,这里会返回一个默认的值

    /**
     * 设置初始值并返回值
     */
    private T setInitialValue() {
    	//调用initialValue设置初始值,这里直接返回null,子类可覆盖重写该方法
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程对应ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);	//若map不为null  将值set进map中
        else
            createMap(t, value);	//初始化map并指定值
        return value; //返回创建的初始值
    }

就是这个样子,然后我们分析一下map不为null的时候,这是会调用ThreadLocalMap的get方法获取值,传入了当前线程。

        /**
         * 根据key查找数据  返回对应Entry
         */
        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	//e为空或e不是当前节点 会调用getEntryAfterMiss方法进行处理
                return getEntryAfterMiss(key, i, e);
        }

如上注解所示。在看一下getEntryAfterMiss方法的处理

        /**
         * 未直接查询到数据的操作
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;	//获取数组
            int len = tab.length;	//获取数组长度

            while (e != null) {	//遍历节点,直到e == null
                ThreadLocal<?> k = e.get();	//获取节点对应的ThreadLocal k
                if (k == key)	//判断是否为当前ThreadLocal对象key
                    return e;	//若是当前key 返回当前节点
                if (k == null)	//若k为null
                    expungeStaleEntry(i);	//从i开始清除无用的节点
                else
                    i = nextIndex(i, len);	//获取下一个节点下标
                e = tab[i];	//获取下标对应数据
            }
            //走到这里说明未找到对应节点的数据
            return null;	//返回null
        }

又看到了这个expungeStaleEntry方法,到这里我们看出ThreadLocal在调用get方法的时候也会对Thread中的ThreadLocalMap对象上的数组进行一次清理。

3.3 ThreadLocal中的set方法

这里的set方法也会处理ThreadLocalMap中的无效数据的,我们也来看一下它是如何进行的

    /**
     * 设置值
     */
    public void set(T value) {
    	//获取当前线程
        Thread t = Thread.currentThread();
        //获取Thread类中维护的ThreadLocalMap 默认是null
        ThreadLocalMap map = getMap(t);
        //若map不为null 调用其set方法
        if (map != null)
            map.set(this, value);
        else
        	//map 为null  调用createMap方法
            createMap(t, value);
    }

第一次set时createMap这里不用说,肯定时没有进行清理的,这时候肯定也是不需要清理的,我们看一下map.set方法

        /**
         * 设置值的set方法
         */
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table; //获取Entry数组
            int len = tab.length;	//获取当前数组大小
            int i = key.threadLocalHashCode & (len-1);//计算下标

            for (Entry e = tab[i];	//获取e
                 e != null;	//e不为null
                 e = tab[i = nextIndex(i, len)]) {	//获取下一个位置  0到len-1循环
                ThreadLocal<?> k = e.get();	//获取k

                if (k == key) {	//若key相等  覆盖value值
                    e.value = value;
                    return;	//返回
                }

                if (k == null) {	//若节点为空
                    replaceStaleEntry(key, value, i);	//调用replaceStaleEntry方法处理
                    return; //直接返回
                }
            }
            //走到这里说明 (e = tab[i]) == null 
            tab[i] = new Entry(key, value);	//直接将数据放入
            int sz = ++size;	//计算新的大小并赋值给sz
            if (!cleanSomeSlots(i, sz) && sz >= threshold)	//没有节点可清理并且sz达到扩容阈值
                rehash();	//扩容操作
        }

这里计算key对应到Entry数组中的下标位置,然后从当前节点进行检查,结果分以下情况:

1,若查找到key相同的节点,则覆盖value

2,若找到节点非空,但其key为空,则替换该节点信息

3,若找到一个null节点,就新建节点放入这个位置

最后还要检查当前是否需要扩容,若需要,则进行扩容操作

下面我们先看一下replaceStaleEntry方法的内容

        /**
         * 替换节点
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;	//获取数组
            int len = tab.length;	//获取数组长度
            Entry e;

            int slotToExpunge = staleSlot;	//当前下标
            /**
             * 从staleSlot前一个位置向前遍历数据直到出现一个null节点
             * 	最后slotToExpunge记录staleSlot以及位置之前这一批非null节点中的第一个key为null的节点
             */
            for (int i = prevIndex(staleSlot, len);	//获取前一个下标
                 (e = tab[i]) != null;//循环条件tab[i] != null
                 i = prevIndex(i, len))//向前遍历
                if (e.get() == null)	//若节点的弱引用值为null
                    slotToExpunge = i;	//记录当前下标

            /**
             * 从staleSlot下一个位置向后遍历数据直到出现一个null节点
             */
            for (int i = nextIndex(staleSlot, len);	//从当前下标向后
                 (e = tab[i]) != null;	//循环条件tab[i] != null
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();//获取k

                if (k == key) { //若key == key  
                    e.value = value;	//覆盖值

                    tab[i] = tab[staleSlot];	//将staleSlot位置的数据赋值到i位置
                    tab[staleSlot] = e;			//将e节点放到staleSlot位置上

                    //slotToExpunge == staleSlot说明,staleSlot之前未出现节点非空但key为null的数据
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;	//记录slotToExpunge的值为i
                    //清除数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;	//返回结果
                }
                //slotToExpunge == staleSlot说明,staleSlot之前未出现节点非空但key为null的数据
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
            
            //情况之前的几点数据
            tab[staleSlot].value = null;
            //在staleSlot位置放入新的节点
            tab[staleSlot] = new Entry(key, value);

            //slotToExpunge != staleSlot说明,staleSlot之前出现了节点非空但key为null的数据
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//清除数据
        }

然后看一下cleanSomeSlots方法的处理

        /**
         * 清除一些过期数据
         */
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;	//设置removed = false
            Entry[] tab = table;	//获取Entry数组
            int len = tab.length;	//获取数组长度
            //循环
            do {
                i = nextIndex(i, len);	//获取下一个节点位置
                Entry e = tab[i];	//获取节点数据
                if (e != null && e.get() == null) { //若数据存在但已无效
                    n = len;	//设置n为数组长度
                    removed = true;	//设置removed为ture
                    i = expungeStaleEntry(i);	//调用expungeStaleEntry方法清除无用的Entry
                }
            } while ( (n >>>= 1) != 0);	//n 无符号右移 1位不为0
            return removed;
        }

最后就是扩容方法

        /**
         * 扩容操作
         */
        private void rehash() {
            //先调用expungeStaleEntries方法清理数控
        	expungeStaleEntries();

        	//清理数据之后还存在 size >=  threshold * 3 / 4
            if (size >= threshold - threshold / 4)
                resize(); //调用resize方法进行扩容
        }


        /**
         * 清理数据
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;	//获取数组
            int len = tab.length;	//获取大小
            for (int j = 0; j < len; j++) {	//遍历数组
                Entry e = tab[j];	//获取节点信息
                if (e != null && e.get() == null)	//若节点存在但无效
                    expungeStaleEntry(j);	//调用expungeStaleEntry方法清理数据
            }
        }


        /**
         * 扩容操作
         */
        private void resize() {
            Entry[] oldTab = table;	//获取数组
            int oldLen = oldTab.length;	//获取大小
            int newLen = oldLen * 2;	//获取新大小  是原大小的2倍
            Entry[] newTab = new Entry[newLen];	//创建新的数组
            int count = 0;	//设置count为0

            //遍历数组
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];//获取节点信息	
                if (e != null) {	//节点非空
                    ThreadLocal<?> k = e.get();	//获取对应key
                    if (k == null) {	//key为空
                        e.value = null; //清除对应value
                    } else {	//key非空的情况
                        int h = k.threadLocalHashCode & (newLen - 1);//计算数据在新数组中的位置
                        while (newTab[h] != null)	//从当前位置开始向后查找到第一个空节点位置h
                            h = nextIndex(h, newLen);
                        newTab[h] = e;	//将节点放入到h位置
                        count++;	//计算器+1
                    }
                }
            }

            //设置新的扩容阀值
            setThreshold(newLen);
            size = count;	//将count赋值给size
            table = newTab;	//将新数组赋值给table
        }

关于ThreadLocal就先到这里吧

猜你喜欢

转载自blog.csdn.net/luo_mu_hpu/article/details/108002420
今日推荐