欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ThreadLocal内存泄漏分析

程序员文章站 2022-05-05 21:45:41
...

一、ThreadLocal结构

每个Thread都有一个ThreadLocalMap类型的成员变量threadLocals,ThreadLocalMap中有一个Entry[ ]类型的成员变量table,Entry保存对threadLocal对象的引用和对应set进去的值,Entry本身继承了WeakReference。
ThreadLocal更像一个工具,ThreadLocalMap实现了对内部Entry数组的操作,每一个Entry就像一个k/v键值对,保存着threadLocal引用和set进来的值。

//示例代码
ThreadLocal<Person> threadLocal=new ThreadLocal<>();

ThreadLocal内存泄漏分析

二、使用弱引用的原因

当threadLocal不再指向new ThreadLocal<>()这块内存时,如果Entry中有一个强引用指向这块内存,那么new ThreadLocal<>()这个对象就不会被回收,造成内存泄漏。

三、内存泄漏原因

当threadLocal对象被回收时,如果没有将之前set进去的对象移除,那么被set进去的对象还是会被Entry中的value引用,而且table也被线程中的ThreadLocalMap引用,导致之前set进去的对象不能被回收,造成内存泄漏。为了尽量避免这个问题,在每次对threadLocal进行set和get操作时,会检查用来存放Entry的table数组内是否有key为空的元素(也就是判断是否能通过Entry的get方法获取到弱引用所指向的threadLocal对象),如果存在这样的元素,那么会将Entry内的value置为null,并且将对应的table[i]置为null,这样在下次gc时就可以回收之前set进去的对象,并且也会清除table数组中无效的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();
                
				//如果之前已经set过了则覆盖之前的值
                if (k == key) {
                    e.value = value;
                    return;
                }

				//如果当前位置的Entry指向的threadLocal对象已经被回收
				//代替这个无效元素并进行一次对无效元素的扫描
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            //先执行cleanSomeSlots尝试扫描数组中的无效元素并清除
            //如果没有找到无效的元素那么判断table的size是否超过阈值,如果超过则扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

		private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                //如果Entry的弱引用指向的threadLocal对象已被回收,则此Entry为无效元素,需要被清除
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

		private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot  清除指定位置的entry
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                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;
        }

即使这样还是会造成内存泄漏。在调用set方法时,会从当前set进去的位置开始向后扫描无效元素并清除,如果在指定的几次扫描后没有扫描到失效元素的位置,那么失效的Entry及set进去的对象就会一直占用内存,而且如果不经常调用set或get方法则更容易出现内存泄漏。除非线程运行的时间很短,只有线程运行结束后相应的内存才会被释放。
由于ThreadLocal内部扫描无效元素的不确定性,我们在不使用threadLocal时应及时调用remove方法释放内存。

相关标签: 多线程