ThreadLocal到底会不会导致内存泄漏?

ThreadLocal一般用来在同一个线程中传递参数。不同线程中的ThreadLocal参数是隔离的。那么TheadLocal是否会导致内存泄漏呢?我们一起来分析。

一、ThreadLocal、ThreadLocalMap、Thread源码分析

1.ThreadLocal中有个静态内部类ThreadLocalMap

在这里插入图片描述

2.ThreadLocalMap中又有个静态内部类Entry,Entry继承WeakReference<ThreadLocal<?>>类,说明Entry是一个弱引用(如果只有弱引用存在,那么在GC的时候就会被回收)。Entry的源码如下

static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

Entry的构造方法的有两个参数,可以看出来一个是ThreadLoca<?>类型的k,传给了父类WeakReference

3.在ThreadLocalMap中定义了一个Entry[]数组类型的变量table。源码如下:

private Entry[] table;

4.在ThreadLocalMap的构造方法中对table变量进行了初始化,源码如下

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
    	//计算门槛值
        setThreshold(INITIAL_CAPACITY);
    }

从以上源码可以看出,在构造ThreadLocalMap对象的时候做了如下几件事

  • table进行初始化,初始化大小为16;
  • 计算ThreadLocal<?>变量firstKey在数组中的位置i
  • 根据firstKey和 firstValue构造一个Entry对象,并赋值给table[i]。
  • 设置size为1
  • 计算一个门槛值

5.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);

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

从以上源码可以看出,在set方法中做了如下几件事

  • 计算ThreadLocal<?>变量firstKey在数组中的位置i

  • 进入循环操作:判断数组中的位置i是否已经创建Entry对象,

    • 如果已经创建Entry对象
      • 先判断已经创建的Entry对象的key是否等于当前的传入的ThreadLocal<?>对象,如果等于,则直接结束set方法
      • 再判断已经创建的Entry的key是否为null,如果为空则根据set方法的参数重新创建Entry对象赋值给table[i],并且直接结束set方法
      • 以上都不是,则根据i和len计算下一个i的值,并获取新的table[i],赋值给对象e,重新进入上面的循环
    • 如果当前的table[i]没有创建Entry对象,则退出循环逻辑
  • 根据set方法的参数创建Entry对象赋值给table[i]

  • 根据firstKey和 firstValue构造一个Entry对象,并赋值给table[i]。

  • 设置size++

  • 判断是否进行Entry数组的扩容

6.TheadLocal中set方法源码如下

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //从当前线程中获取 ThreadLocalMap
     ThreadLocalMap map = getMap(t);
    
    if (map != null)
        //如果map不为空则添加当前的threadLocal对象和value
        map.set(this, value);
    else
        //如果map为空则为当前线程t,初始化ThreadLocalMap对象
        createMap(t, value);
}

从以上源码可以看出,set方法做了如下几件事

  • 获取当前线程t;

  • 从当前线程中获取 ThreadLocalMap类型的threadLocals属性,从getMap方法的代码可以看出,Thread类中有个ThreadLocalMap类型的属性threadLocals,但是ThreadLocalMap这个类型是定义在了ThreadLocal中的,是它的静态内部类。getMap方法的代码如下

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    

    在Thread类中可以看到threadLocals属性的类型为ThreadLocal.ThreadLocalMap

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
  • 如果map不为空则添加当前的ThreadLocal对象this和传入的value进map数组中。ThreadLocalMap的set方法在上面的第5点已经说明。

  • 如果map为空则调用createMap方法,从createMap的代码可以看出就是给传入的线程t初始化threadLocals属性,ThreadLocalMap的构造方法在上面第4已经说明。代码如下

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    

7.TheadLocal中get方法源码如下

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //根据当前的ThreadLocal对象获取map中的Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
             //返回Entry的value
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

从以上源码可以看出,在get方法做了如下几件事

  • 获取当前线程;
  • 获取当前线程的ThreadLocalMap
  • 根据当前的ThreadLocal对象获取map中的Entry对象
  • 返回Entry的value

根据以上源码分析可知,ThreadLocal类中其实没有存任何属性用来存数据,所有的数据都是存在当前线程Thread中threadLocals属性中,threadLocals是ThreadLocal.ThreadLocalMap类型。ThreadLocal.ThreadLocalMap类对象定义在了ThreadLocal类中,并且是它的一个静态内部类。ThreadLocal.ThreadLocalMap类似一个HashMap,其中的Key 是一个弱引用的ThreadLocal<?>类型,value是当前方法传入的值。我们平常操作ThreadLocal中的set和get方法操作其实是操作当前线程的threadLocals属性。

二、结论

1.如果TreadLocal作为局部变量使用(现实情况中很少当局部变量用),三者在JVM中的关系图如下

在这里插入图片描述

当threadLocal局部变量使用完出栈之后,图例如下

在这里插入图片描述

从上图可以看到堆中的threadLocal 对象会只存在虚引用,当GC的时候它就会被回收掉。所以当ThreadLocal当作局部变量使用的话,严格意义上不会存在内存泄漏。因为只要GC堆中的ThreadLocal就会被回收。

2.如果TreadLocal作为静态变量使用,静态变量存在方法区,那么上面的图会有一个从方法区的变量实线连接到堆中的ThreadLocal内存对象,大家脑补一下图。堆中的ThreadLocal对象会一直被静态变量所引用,就不会被回收。此时也不会存在内存泄漏。

3.如果TreadLocal在线程池中使用

  • 如果该线程属于核心线程,它不会被销毁。当ThreadLocal对象在当前线程的业务使用完成之后,不调用remove方法,那么当前线程的threadLocals中的ThreadLocal和Value就不会被删除和回收。该线程回到线程池中后,当下一个业务使用线程池再次使用到之前的线程时,通过ThreadLocal会拿到之前使用该线程的业务参数。可能会导致业务参数混用。严格来说这种情况是一种内存泄漏。所以这种情况下需要手动remove。
  • 如果该线程不属于核心线程,它可能会被销毁。如果不被销毁那么和上面的结论一样。如果被销毁了,那么线程中的threadLocals会设置为null。此时也不存在内存泄漏。
  • 业务结束之后都需要手动调用一下remove。
  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值