ThreadLocal内存泄漏问题

ThreadLocal原理

阅读本文章之前,需要先了解Java中强软弱虚的概念,传送地址:Java中强软弱虚四种引用

一、介绍

  • 可以解决多线程的数据安全问题,将当前线程关联一个数据(可以是普通变量,可以是对象,也可以是数组,集合等)
  • 每个线程只能存取本线程的数据,无法操作其他线程的数据,各个线程的数据互相独立
  • ThreadLocal中定义了 set、get、remove 方法,用来关联 / 取出 / 移除数据
  • 每一个 ThreadLocal 对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个ThreadLocal 对象实例

二、快速入门

public class ThreadLocalTest {
	public static void main(String[] args) throws InterruptedException {
		
        //创建ThreadLocal对象实例
		final ThreadLocal<Integer> th = new ThreadLocal<Integer>();

		//t1线程
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					th.set(100); //t1线程存放数据
					System.out.println("t1线程赋予的值:" + th.get());
					Thread.sleep(2000); //t1线程睡眠2秒,睡眠的时间段内t2线程会运行
					System.out.println("t1线程获取的值:"+th.get());
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
		
		Thread.sleep(1000);

		//t2线程
		new Thread(new Runnable() {
			@Override
			public void run() {
				Integer ele = th.get(); //t2线程取出数据(此时为null,与t1无关系)
				System.out.println("t2线程获取的值:" + ele);
				th.set(200); //t2线程赋值
				System.out.println("t2线程赋值后获取的值:"+th.get()); //t2线程从自己的副本中获取值
			}
		}).start();
	}
}

运行结果:

image-20210721171230454

三、ThreadLocal与synchronized的区别

  • 原理不同
    • synchronized
      • 只提供了一份数据,让不同的线程竞争锁访问
    • ThreadLocal
      • 为每一个线程都提供了各自的数据,从而实现同时访问互不干扰
  • 侧重点不同
    • synchronized
      • 多个线程之间访问资源的同步
    • ThreadLocal
      • 多线程中让每个线程之间的数据相互隔离

四、ThreadLocal内部结构

  • 每个 Thread 维护一个 ThreadLocalMap,这个Map的 keyThreadLocal 实例本身,value 是真正要存储的值

    image-20210721171604754
  • 图示

    image-20210721191903457

    Map由Entry键值对组成

    Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用

五、ThreadLocal核心方法源码

1. set方法

  • 源码和对应的中文注释

    /**
      * 参数是Map中的value值
      */
    public void set(T value) {
    
        // 获取当前线程对象
        Thread t = Thread.currentThread();
    
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
    
        // 判断map是否存在
        if (map != null)
            // 存在则调用set方法,this表示此线程中TheadLocal实例
            // 创建Entry并赋值
            map.set(this, value);
        else
            // 如果当前线程不存在ThreadLocalMap对象,则创建Map
            createMap(t, value);
    }
    
    /**
      * 被调用的getMap方法
      * @param 当前线程
      * @return 对应维护的ThreadLocalMap 
      */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    /**
      * 被调用的createMap方法
      * @param 当前线程
      * @param 存放到Map中的第一个entry的值
      */
    void createMap(Thread t, T firstValue) {
        //this是ThreadLocal实例
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
  • 代码执行流程

    • 首先获取当前线程,并根据当前线程获取一个Map
    • 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key),创建Entry并赋值
    • 如果Map为空,则给该线程创建Map,并设置初始值,初始值即为参数值

2. get方法

  • 源码和对应的中文注释

    /**
      * 返回当前线程的Map中的value值
      */
    public T get() {
    
        // 获取当前线程对象
        Thread t = Thread.currentThread();
    
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
    
        // 如果此map存在
        if (map != null) {
    
            // 以当前的ThreadLocal为key,调用getEntry获取对应的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
    
            // 如果Entry不为空,返回对应的value值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
    
        //有两种情况执行setInitialValue()方法
        //第一种情况: map不存在,表示此线程没有维护ThreadLocalMap对象
        //第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
        return setInitialValue();
    }
    
    /**
      * 被调用的setInitialValue方法,创建Entry并返回对应的value值,默认为null
      */
    private T setInitialValue() {
    
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
    
        // 获取当前线程对象
        Thread t = Thread.currentThread();
    
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
    
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1.当前线程Thread不存在ThreadLocalMap对象
            // 2.则调用createMap进行ThreadLocalMap对象的初始化
            // 3.并将t(当前线程)和value(默认为null)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    
        // 返回设置的值value,默认为null
        return value;
    }
    
    
  • 代码执行流程

    • 首先获取当前线程, 根据当前线程获取一个Map
    • 如果获取的 Map 不为空,则在Map中以 ThreadLocal 的引用作为key在Map中获取对应的 Entry e
    • 如果 e 不为空,则返回 e.value
    • 如果Map为空或者e为空,则通过 initialValue() 函数获取初始值 value(默认为null),然后用ThreadLocal的引用和value作为 firstKey 和 firstValue 创建一个新的 Map
  • 总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

六、弱引用的使用

Entry对应的源码如下:

//Entry继承自一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {

    //属性value
    Object value;

    //构造器
    Entry(ThreadLocal<?> k, Object v) {
        super(k); //父类会创建一个弱引用,参数是ThreadLocal对象
        value = v;
    }
}

//被调用的父类方法
public WeakReference(T referent) {
    super(referent);
}

//被调用的父类方法,第二个参数引用队列传递null值
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

通过上述源码得知,创建Entry的时候会创建一个弱引用,并指向当前线程的ThreadLocal对象,如下图所示:

image-20210721184813095

1. 为什么要使用弱引用?

如果Entry中的key使用强引用指向ThreadLocal对象,当ThreadLocal实例 threadLocal = null; 时,由于ThreadLocal对象仍然被Entry中的key强引用,所以ThreadLocal对象不会被垃圾回收,存在内存泄漏。

内存泄漏:对象不会被GC所回收,然而它却占用内存。

如果使用弱引用,当垃圾回收时,ThreadLocal对象被回收,可以解决ThreadLocal对象内存泄漏的问题。

2. 使用弱引用后是否依然存在内存泄漏?

当ThreadLocal对象被回收后,Entry中的key为null,无法获取value的值,无法回收,所以value指向的内存空间仍然存在内存泄漏问题,故应该使用 remove() 方法将此条Entry记录删除。

3. 线程池归还线程必须清理Map

如果线程池中的线程归还时,没有清理ThreadLocalMap,则此线程被再次使用的时候,可能会导致Map中的数据发生错误,比如发现Map中已经有同名的key,则不会再插入新值等情况。