ThreadLocal 系列之 ThreadLocal 会内存泄漏吗?

相关文章:

谈到 ThreadLocal 在使用的时候的注意事项,其中有一个就是内存泄露,那么 ThreadLocal 会发生内存泄漏吗?或者说什么情况下 ThreadLocal 会内存泄漏呢?

存入超大对象

无须多言,存入“超大对象”肯定会有内存泄漏的风险。

key 为 null 造成内存泄漏

这里引用一篇博客的观点:

Threadlocal 里面使用了一个存在弱引用的 Map,当释放掉 Threadlocal 的强引用以后,Map里面的 value 却没有被回收,而这块 value 永远不会被访问到了, 所以存在着内存泄露。

https://my.oschina.net/u/658658/blog/1830812

很多文章都是这个观点,主要说法是:由于 ThreadLocal.ThreadLocalMap.Entry 中的 key 是弱引用,会存在 keynull 的情况,这时候 value 无法被访问到,但是由于 valueEntry 关联,最主要原因是大部分情况下会使用线程池,造成 value 所占内存无法被释放,从而导致内存泄漏。

其实我们一般使用都会将 ThreadLocal 作为一个类级别变量,会被强引用,这样使用是不会出现"keynull 的情况"。

但是不可否认的是,如果将 ThreadLocal 作为线程池中线程的局部变量的确会出现"keynull 的情况"。可以先看一个例子:

public class ThreadLocalOomDemo1 {

    private static final ExecutorService FIXED_EXECUTOR = Executors.newFixedThreadPool(1);

    public static void main(String[] args)  {

        FIXED_EXECUTOR.submit(() -> {
            ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
                @Override
                protected void finalize() {
                    System.out.println("finalize........");
                }
            };
            threadLocal.set("a");
            System.out.println(threadLocal.get());
        });
        System.out.println("-------------------");
        sleep(100);
        System.gc();
        FIXED_EXECUTOR.submit(()->{
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName());
        });
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

[0.002s][warning][gc] -XX:+PrintGC is deprecated. Will use -Xlog:gc instead.
[0.013s][info   ][gc] Using G1
-------------------
a
[0.296s][info   ][gc] GC(0) Pause Full (System.gc()) 5M->3M(17M) 4.705ms
finalize........
pool-1-thread-1

Debug 调试:
在这里插入图片描述
发现的确会存在出现 keynull ,已被 GC 的情况,此时 value 是无法获取的。

ThreadLocalMap#set

ThreadLocal 中的数据本质是存储到 ThreadLocalMap 中的,看下 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;
            int i = key.threadLocalHashCode & (len-1);
						//出现 hash 冲突,使用开放地址法
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
								// key 相同,覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
								//key 为 null
                if (k == null) {
                  //进入替换方法
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

当出现 hash 冲突的时候,使用开放地址法向后查找,出现 keynull,通过replaceStaleEntry 方法处理。

ThreadLocalMap#get

同样地,再看看 get 方法:

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
      //发生 hash 冲突
        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;
                if (k == null)
                    //删除过期数据
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

也就是说当出现 hash 冲突的时候,get 方法继续查找,同样会清除循环中遇到的过期 entry

总结

  • ThreadLocal 的确会出现内存溢出的情况,但是其实说 ThreadLocal 是否会出现内存泄漏,这个表述本身就有问题,真正应该关注的是在线程池中复用的 Thread 对象的成员变量是否会出现内存泄漏,
  • 不能直接以 key 出现 nullvalue 无法被访问回收作为缘由,我们平常都是将 ThreadLocal 作为类级别变量,几乎不可能会出现 keynull 的情况,即使出现了这样的情况,ThreadLocal 在设计时 通过 getset 方法已经做了 keynull 的优化处理;
  • 使用完 ThreadLocal 后,都应该调用它的 remove 方法,避免可能出现的一段时间数据保留(如存入一个大对象,key GC 后,未出现 hash 冲突,那么这个对象会有一段时间长期存在),也可以避免线程复用时出现脏数据;

References

  • https://www.jianshu.com/p/56f64e3c1b6c
  • https://www.iteye.com/blog/liuinsect-1827012

欢迎关注公众号
​​​​​​在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章