一文搞懂ThreadLocal及相关的内存泄露问题

首先,看一张整体的结构图,来帮助理解
在这里插入图片描述

什么是ThreadLocal

ThreadLocal用于创建线程局部变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题

ThreadLocal的简单使用

package test;

public class ThreadLocalTest {

    static ThreadLocal<String> threadLocal= new ThreadLocal<>();
    
    public static void main(String[] args) {
        Thread t1  = new Thread(()->{
            //设置线程1的本地变量的值
            threadLocal.set("线程1");
            System.out.println(threadLocal.get());
            threadLocal.remove();
            System.out.println("after remove : " + threadLocal.get());
        });

        Thread t2  = new Thread(()->{
        	//设置线程2的本地变量
            threadLocal.set("线程2");
            System.out.println(threadLocal.get());
            threadLocal.remove();
            System.out.println("after remove : " + threadLocal.get());
        });

        t1.start();
        t2.start();
    }
}

输出结果:
在这里插入图片描述
由此可知,虽然threadLocal为成员变量,即线程共享,但使用threadLocal在不用线程进行赋值、获取等操作时,不同线程的操作互不相扰。
那么ThreadLocal是如何作为成员变量,却能在实现多线程下数据不共享,作为线程局部变量?

ThreadLocal的原理

回到一开始的图:
在这里插入图片描述

  • ThreadLocal并不实际存储数据,而是作为一个工具类,提供get、set方法根据当前线程用于操作当前线程中的实际数据
  • 每个Thread(线程)中都持有一个ThreadLocal.ThreadLocalMap类型的成员变量,初始值为null。ThreadLocal的get、set方法实际上就是在操作这个成员变量。
  • ThreadLocalMap持有一个Entry[]类型的成员变量table,类似JDK1.7的HashMap中的Entry[] table 可以类比学习
  • Entry key为ThreadLocal<?>类型的的弱引用,value为Object类型的强引用

ThreadLocal的set()

public void set(T value) {
    //1、获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //2、获取当前线程的ThreadLocalMap变量
    ThreadLocalMap map = getMap(t);
    //3、如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //4、如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
	//获取线程的threadLocals
    return t.threadLocals; 
}

void createMap(Thread t, T firstValue) {
	//创建线程的threadLocals, this为ThreadLocal<?>的引用
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal的get()

在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。
引用自:https://www.cnblogs.com/fsmly/p/11020641.html#_label0

public T get() {
    //1、获取当前线程
    Thread t = Thread.currentThread();
    //2、获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //3、如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //4、执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}

private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}

ThreadLocal操作总结

根据上述源码的分析可知:ThreadLocal最终操作的都是调用线程的ThreadLocalMap成员变量,因此不同线程使用同一个ThreadLocal成员变量互不相扰。
每个线程内部有一个ThreadLocal.ThreadLocalMap类型threadLocals的成员变量,该变量的类型为类似于HashMap,其中的key为当前定义的ThreadLocal变量的this引用,value为使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

ThreadLocal使用不当造成内存泄漏问题

弱引用

弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null
在这里插入图片描述

ThreadLocalMap内部实际上是一个Entry数组:

  • Entry是继承自WeakReference(弱引用)的一个类。
  • Entry的key是指向ThreadLocal的弱引用,value一般为强引用

当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录(Entry),这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。
如果当前线程一直存在且没有调用该ThreadLocal的remove方法,此时存在两种情况:

  • ThreadLocalMap之外存在ThreadLocal的引用:那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,无法进行垃圾回收,导致这些本地变量一直存在,可能会出现内存溢出
  • ThreadLocalMap之外不存在ThreadLocal的引用:因为ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,所以下一次垃圾回收时ThreadLocal(key)将被回收。此时ThreadLocalMap中就可能存在key为null但是value不为null的现象,出现内存泄漏。

因此每次使用完ThreadLocal,建议调用它的remove()方法,清除数据,避免内存泄漏

内存泄漏问题总结

ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLocal存在依赖时,ThreadLocalMap中的ThreadLocal对象(即key)就会被回收,而如果其他地方存在ThreadLocal的引用则不会被回收。当key被回收时,Map中就可能存在key为null但是value不为null的现象,出现内存泄漏。

因此每次使用完ThreadLocal,建议调用它的remove()方法,清除数据,避免内存泄漏。

参考

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值