阅读 199

美团二面:来聊聊ThreadLocal内存溢出问题!

前言

上次有个小伙伴问我,说他面试的时候,被问到ThreadLocal内存溢出问题,没有回答出来;那我们今天就来了解一下ThreadLocal。

ThreadLocal介绍

多线程在访问同一个变量时会产生线程安全问题,ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示

image.png

ThreadLocal使用

开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示

public class ThreadLocalTestDemo {
	static ThreadLocal<String> localVar = new ThreadLocal();
	static void print(String str) {
	//打印当前线程中本地内存中本地变量的值
	System.out.println(str + " :" + localVar.get());
	/清除本地内存中的本地变量
	localVar.remove();
}
public static void main(string[] args) {
Thread t1 =new Thread(new Runnable() {
aoverride
public void run() {
//设置线程1中本地变量的值
localVar.set( "localVar1");
//调用打印方法
print( "thread1");
//打印本地变量
System.out.println("thread1 after remove : " + localVar.get());
}
});
Thread t2 =new Thread( new Runnable() {
	@0verride
		public void run() {
		//设置线程2中本地变量的值
		localvar.set( "localVar2" );
		//调用打印方法
		print("thread2");
		//打印本地变量
		System.out.println("thread2 after remove : " + localVar.get());
	}
});
t1.start();
t2.start();
  }
}
复制代码

执行结果

thread1 :localVar1
thread2 :localVar2
thread1 after remove : null
thread2 after remove : null
复制代码

上面代码能够看出,虽然2个线程都使用了ThreadLocal localVar变量,但是线程里面的变量互相不影响。

ThreadLocal原理

我们来看看ThreadLocal是怎么实现的?我们来看一下源码

set源码

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

 在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals,该方法代码如下

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}
复制代码

如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前ThreadLocal引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示

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

createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。

get源码

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){
			aSuppresswarnings ( "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;
}
复制代码

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

remove实现

public void remove{
	ThreadLocalMap m= getMap(Thread.currentThread();
	if (mnull) {
		m.remove( key: this);
	}
}
复制代码

remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量

溢出分析

每个线程内部有一个名为threadLocals的成员变量该变量的类型为 ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

看下图:

image.png

番外篇

1、ThreadLocalMap 内部 Entry 中 key 使用的是对 ThreadLocal 对象的弱引用,这是为了避免内存泄露,因为如果是强引用,那么即使其他地方没有对 ThreadLocal 对象的引用,ThreadLocalMap 中的 ThreadLocal 对象还是不会被回收,而如果是弱引用则这时候 ThreadLocal引用是会被回收掉的

2、但是对于的 value 还是不能被回收,这时候 ThreadLocalMap 里面就会存在 key 为 null 但是 value 不为 null 的 entry 项,虽然 ThreadLocalMap 提供了 set,get,remove 方法在一些时机下会对这些 Entry 项进行清理,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露,所以在使用完毕后即使调用 remove 方法才是解决内存泄露的最好办法。

3.线程池里面设置了 ThreadLocal 变量一定要记得及时清理,因为线程池里面的核心线程是一直存在的,如果不清理,那么线程池的核心线程的 threadLocals 变量一直会持有 ThreadLocal 变量。

总结

希望这篇文章能够帮助小伙伴们理解ThreadLocal,同时不要忘了三连支持!谢谢!!!

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. 关注后回复【666】扫码即可获取架构进阶学习资料包
文章分类
后端