ThreadLocal造成内存泄漏的原因

答案:

举例:ThreadLocal实例的生命周期跟随方法。

原因:

  • 若ThreadLocal实例 的生命周期短于 threadLocals 的生命周期,就会出现内存泄漏。
  • ThreadLocal实例可在任何地方创建,若在方法中创建,生命周期跟随方法,随着方法出栈而销毁。
  • threadlocals 属于Thread类,生命周期跟随线程,若是核心线程,生命周期会很长。
  • threadLocals 类型是ThreadLocal.ThreadLocalMap,底层是Entry[]数组,Entry的key是弱引用,若key指向的对象没有其他强引用指向,只要gc就会删除对象。

Thread、threadLocals、ThreadLocal 的关系:

  • Thread类有成员属性 threadLocals,类型是 ThreadLocal.ThreadLocalMap。
  • ThreadLocalMap 是 ThreadLocal 的静态内部类,底层是 Entry[] table数组。
  • ThreadLocal的实例.set(value),相当于向执行此行的线程的 threadLocals属性put(ThreadLocal的实例,value);

结论:

  • 若 ThreadLocal实例在方法中创建,随着方法出栈销毁,线程的threadLocals中会出现(null, value)的Entry对象。
  • key 为 null 的 Entry 对象,无法被使用,也就没必要存在。
  • 由于 Entry[] 数组指向这个对象,导致 key 为 null 的Entry 对象无法被删除。
public class ThreadLocalTest {

    public static void main(String[] args)throws Exception{
        Thread thread = Thread.currentThread();
        ThreadLocalTest test = new ThreadLocalTest();
        test.test1();
        System.gc();//test1执行完出栈,test1方法中的 threadLocal1、threadLocal2 指向的对象,gc时会被回收
        TimeUnit.SECONDS.sleep(1);
        System.out.println(thread);//debug 观察当前线程的 threadLocals 属性值:1、2对应的entry的key变成null
    }

    //entry的key为什么用弱引用?
    //若不用弱引用,会导致方法执行完 threadLocal1、threadLocal2,无法被gc
    //因为,Entry实例的key指向这个ThreadLocal实例
    public void test1(){
        ThreadLocal threadLocal1 = new ThreadLocal();
        threadLocal1.set(1);
        ThreadLocal threadLocal2 = new ThreadLocal();
        threadLocal2.set(2);
        Thread thread = Thread.currentThread();
        System.out.println(thread);//debug 观察当前线程的 threadLocals 属性值:1、2对应的entry的key有值
    }
}

分析:

1、什么是Entry[] table数组?

Thread 类的成员变量 threadLocals 的类型是 ThreadLocal.ThreadLocalMap。

ThreadLocalMap类是 ThreadLocal 类的静态内部类。

ThreadLocalMap底层数据结构是  Entry[] 数组。

ThreadLocal threadLocal1 = new ThreadLocal();
threadLocal1.set(1);
threadLocal1.set(1); 
就是向,执行这行代码的线程 的 threadLocals属性,put(threadLocal1,1)。
也就是向 Entry[] 数组添加 Entry(threadLocal1,1) 对象。

2、Entry类的对象存什么?

Entry类存一对键值对,key是 ThreadLocal 对象的弱引用,value是 Object对象的强引用。

  • 弱引用特点:只有弱引用指向的对象,jvm一触发垃圾回收,就会回收此对象。
  • 强引用特点:强引用指向的对象,jvm垃圾回收不会回收此对象。
  • 所以,只要jvm触发垃圾回收,就会存在key为null的entry对象。
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

3、为什么Entry[] table数组一直存在?

开发中一般用线程池管理线程,核心线程会一直运行。那么核心线程的 threadLocals 属性指向的 ThreadLocalMap 对象会一直存在,ThreadLocalMap 底层的 Entry[] table 数组也会一直存在。

4、为什么 Entry[] table数组中索引指向的“key为 null 的 entry对象”不会删除?

直接看更新源码:

1、更新时,会先遍历 Entry[] 数组,判断key是否存在,存在则覆盖value。

  • 传入的key肯定不为null,key也就是ThreadLocal的对象,通过此对象调用set方法,那此对象肯定不为null。
  • 所以这边不会命中 key为null的Entry对象。

2、若Entry[] table数组 不存在此key的Entry对象,则创建新的entry对象插入Entry[] table数组。

所以,更新方法不会操作key为null的Entry对象。

//修改当前线程的 ThreadLocalMap
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(1);
//ThreadLocal 部分源码 
public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap对象
        if (map != null) {
            map.set(this, value);//存在则更新
        } else {
            createMap(t, value);//不存在,创建ThreadLocalMap对象,再更新
        }
    }



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)]) { //遍历Entry[]数组
                ThreadLocal<?> k = e.get();

                if (k == key) { //比较key是否存在,存在则覆盖value
                    e.value = value;
                    return;
                }

                if (k == null) { //这是针对内存泄漏的解决方法
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);//不存在,创建新的Entry对象添加到数组中
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold) //判断数组长度是否要扩容
                rehash();
        }

5、如何解决?

ThreadLocal 类的 set 方法中,判断key为null,则将Entry[] table数组对应的索引指向null。

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: ThreadLocal造成内存泄漏原因是由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应的key就会导致内存泄漏,而并不是因为弱引用。\[1\]因此,当ThreadLocal对象被回收后,对应的value仍然存在于ThreadLocalMap中,无法被回收,从而导致内存泄漏。\[1\]但是,只要规范化使用ThreadLocal,并在不需要的时候手动删除对应的key,就可以避免内存泄漏的问题。\[2\]ThreadLocal的实现原理是每个Thread维护一个ThreadLocalMap对象,其中key为弱引用ThreadLocal对象,value为线程变量的副本。\[3\]因此,ThreadLocal并不是洪水猛兽,只要正确使用并遵循规范,就可以避免内存泄漏的问题。\[2\] #### 引用[.reference_title] - *1* *3* [ThreadLocal内存泄漏原因,如何避免](https://blog.csdn.net/weixin_44356698/article/details/118547982)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v4^insert_chatgpt"}} ] [.reference_item] - *2* [threadlocal内存泄漏原因](https://blog.csdn.net/oliver486/article/details/123798272)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v4^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值