ThreadLocal的内存泄露真的存在?(第一话)
共 8067字,需浏览 17分钟
·
2021-08-24 23:08
内存泄露?
ThreadLocal的前世今生
ThreadLocal这个对象相信大家并不陌生,但是这玩意有人说会造成内存泄露(感觉很严重的样子),听到这,怕是有些经验不足的童鞋会直接放弃使用它了。那么基于此,我们就研究一下,ThreadLocal到底是个什么东西,有什么用?又为什么会有人说他会造成内存泄露呢?真的有内存泄露吗?你真正的会用ThreadLocal吗?
如果你对上面的一连串发问,回答时带着好像、似乎、差不多的词汇,建议你看看本文。
01
ThreadLocal是什么?
ThreadLocal就是一个java类,是jdk【java.lang】包下的一个很普通的类。他有一个非常重要的静态子类 ThreadLocalMap,该子类也有一个静态子类叫Entry,可以说ThreadLocal基本就是靠这哥俩,完成他的功能。
02
ThreadLocal有什么用?
A
● 线程级别的变量存储,成为线程的一个全局变量,从而可以让变量在各个方法中均可使用而不用传递参数
B
● 在高并发 多线程环境,实现不同线程之间的数据隔离
很多人说ThreadLocal可以让线程拥有一个共享变量的独立副本,且不同线程间修改其自己的副本,不会影响其他线程。听起来就像是ThreadLocal可以给每个线程拷贝该变量的副本,像Object.clone()一样。经过查看源码,私以为线程间的数据隔离这种说法比较靠谱。实际上,错误的使用ThreadLocal,不理解它的实现机制的话,往往会造成变量的共享。一个线程修改变量,导致其他线程的变量的值也被修改了。
所以在工作中,用ThreadLocal的时候,有人说会造成内存泄露,用的时候就很忐忑,不知道会有什么影响,不太敢用,于是下决心搞明白ThreadLocal到底该怎么用,会有什么问题?
没有调查就没有发言权,如果你也对ThreadLocal很疑惑,那我们就一起来看看它吧。
03
ThreadLocal的用法
我们先从ThreadLocal的基本用法出发,然后再根据代码去一步一步的看源码,这样知道它的来龙去脉,就会好很多,比直接进入这个类里面去看源码,思路要清楚的多。
1跨方法调用
public class Bean {//先定义一个类
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
public class Demo {
private static ThreadLocal<Bean> tl = new ThreadLocal<>();
public static void main(String[] args) {//执行main函数的线程是main线程
Bean bean = tl.get();
System.out.println("直接调用get方法输出:"+bean);
methodA();
methodB();
}
private static void methodA() {
Bean bean = new Bean();
bean.setNum(100);
tl.set(bean);
}
private static void methodB() {
Bean bean = tl.get();
System.out.println(bean.getNum());
}
}
demo很简单,tl成员变量在main线程中被调用,methodA方法中set我们的Bean对象,在methodB方法中获取该对象,然后输出该对象的num值,输出结果如下:
直接调用get方法输出:null
100
如此可以实现变量的跨方法调用,通常在非常复杂的业务逻辑中,在A方法中的变量要在G方法中使用,但是A-G之间的方法又不用这个变量,如果将该变量层层传递,就会显得过于累赘。使用ThreadLocal就会简单的多,看起来Bean变量就变成了main线程的全局变量,只要调用ThreadLocal的set方法以后,在main线程的其他地方就都可以使用了。这是ThreadLocal的其中一个用法。ThreadLocal不一定非要定义为成员变量,也可以在方法中定义。
2线程间的数据隔离
public class Demo2 {
private static ThreadLocal<Bean> tl = new ThreadLocal<Bean>(){
@Override
protected Bean initialValue() {
return new Bean();
}
};
public static void main(String[] args) {
new Thread(()->{
Bean bean = tl.get();
bean.setNum(100);//线程设置num的值 验证另一个线程的num值
}).start();
new Thread(()->{
Bean bean = tl.get();//获取Bean变量
System.out.println(bean.getNum());//输出num值
}).start();
}
}
上面这个例子,首先在成员变量定义了一个ThreadLocal的子类,复写了它的 initialValue()方法,这个方法很重要,待会源码就能看到它了。然后在main函数中开启了两个线程,第一个线程将bean的num设置为100,第二个线程获取到bean以后,再输出num值。最后看到输出结果是0。两个线程都拥有了Bean变量,但是两个bean是不一样的。这样线程修改自己的变量对其他线程的变量就不会造成影响。但是如果将return new Bean();换成一个已经存在的Bean对象,那么结果就完全不一样了。
这样的用法有什么意义呢???在数据库连接和session的管理中很有用。就不在多说了。
了解了用法以后,我们看一下源码,为什么可以这么用?
04
源码解析
01
首先看ThreadLocal的get方法
为什么 直接调用get方法输出:null
public T get() {//ThreadLocal.get()
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取线程的属性:ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
看一下getMap(t)的实现
ThreadLocalMap getMap(Thread t) {//ThreadLocal.getMap()
return t.threadLocals;//直接取线程额threadLocals变量
}
ThreadLocal.ThreadLocalMap threadLocals = null;//Thread类的成员变量
显然 ThreadLocalMap map 是线程的属性 threadLocals ,第一次获取肯定是null,所以要走setInitialValue()方法。暂时先不管ThreadLocalMap 是什么。
private T setInitialValue() {//ThreadLocal.setInitialValue()
T value = initialValue();//demo2中复写的方法
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {//ThreadLocal.initialValue()
return null;//直接new一个ThreadLocal 该方法返回null
}
代码看到此处就很明了了,Demo中我们直接new出来的ThreadLocal,所以调用get方法时,返回的是null,所以 直接调用get方法输出:null;输出没有问题。由于线程的threadLocals变量还未被赋值,所以setInitialValue方法再次调用getMap(t)返回的仍然是null,这时候就要去创建这个ThreadLocalMap 了--createMap(t, value);
02
调用set方法后发生了什么
在Demo中的methodA中set了一个new Bean();我们有必要看一下ThreadLocal的set方法的实现:
public void set(T value) {//ThreadLocal.set()
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
发现这里也有一个createMap(t, value);方法,假如我们在使用ThreadLocal之前没有调用过get方法而直接调用set方法,getMap(t)还是返回null,要走createMap(t, value);所以我们要重点看一下这个方法做了什么事情。
void createMap(Thread t, T firstValue) {//ThreadLocal.createMap()
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
createMap(t, value)让线程的threadLocals指向new出的ThreadLocalMap。此时 getMap(t)就不会再返回null了。
03
ThreadLocal中的数据存到哪了?
然后我们需要看一下new ThreadLocalMap(this, firstValue);的内容,请注意this是当前ThreadLocal的引用,也就是Demo和Demo2中的ThreadLocal<Bean> tl成员变量。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//ThreadLocalMap构造方法
table = new Entry[INITIAL_CAPACITY];//初始化Entry数组 默认16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//计算value应该放在哪个槽
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);//设置扩容的临界值
}
这里又遇到了陌生的Entry类,这个类继承了WeakReference。之前也提到了ThreadLocalMap是ThreadLocal的静态子类,Entry是ThreadLocalMap的静态子类。暂时先不管Entry为什么要继承WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; /** The value associated with this ThreadLocal. */
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;//最终Bean存储到了valuez中
}
}
上面两个构造函数很好理解,ThreadLocalMap构造函数 创造了一个长度为16的Entry数组,然后根据ThreadLocal的hash值确定new出的Entry对象放在哪个数组的哪个下标位置。Entry的构造函数有一个key,一个value。value被存储到Entry对象的属性Object value中。至此告一段落,我们看一下变量的引用关系:
线程持有threadLocals 属性,该属性是ThreadLocalMap对象,ThreadLocalMap内有一个Entry[]数组table,默认16大小,Entry对象持有ThreadLocal和Value,可以看成Entry[key,value]的形式。Entry的弱引用问题待会再讲。
类的关系图
为了便于理解,对象引用的关系如图(不代表实际内存位置)
我们的new出的Bean对象最终存到了Entry对象中的value属性中。
04
ThreadLocalMap的数据结构
所以ThreadLocal的get方法初次调用时,Entry[] table数据结构是这样的:
当我们再调用了set方法后,由于此时线程的threadLocals 属性已经不再为null,就会去调用ThreadlocalMap的set方法遍历Entry数组,判断key是否等于当前的ThreadLocal对象,如果是,将此前的Entry的value属性覆盖为新的value。源码如下:
private void set(ThreadLocal<?> key, Object value) {// map.set(this, 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) {//这里将value值覆盖掉
e.value = value;
return;
}
if (k == null) {//由于弱引用的存在,key可能会被回收,所以要处理
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);//如果entry数组中没有,那就再放进去一个
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();//将Entry数组扩大 类似Hashmap
}
经过Demo中methodA方法的tl.set(bean)之后,value的原来的null值被替换成bean。假如Demo中有一个methodC方法,new了一个新的ThreadLocal并set了值,那么数据结构将如下图所示:
当线程的ThreadLocalMap属性有值的时候,通过ThreadLocalMap.get方法就可以获取到对应的Entry,Entry再调用get方法获取Bean,从而实现变量的存储。
05
ThreadLocal之真假变量拷贝
我们再回顾一下Demo2中,复写了initialValue()方法,线程第一次调用get方法时,返回的都是new的一个新的Bean,所以两个线程的bean是不一样的,从而修改它不会对其他线程产生影响,因为不存在共享。所以在Demo2中,即便第一个线程将num设置为100,对第二个线程的Bean对象没有任何影响,输出0。
如果在Demo2中的return new Bean();换一种写法,就会造成多个线程之间的数据共享。
public class Demo3 {
private static Bean bean = new Bean();
private static ThreadLocal<Bean> tl = new ThreadLocal<Bean>(){
@Override
protected Bean initialValue() {
return bean;
}
};
public static void main(String[] args) {
new Thread(()->{
Bean bean = tl.get();
bean.setNum(100);
}).start();
new Thread(()->{
Bean bean = tl.get();
System.out.println(bean.getNum());
}).start();
}
}
此时输出的结果是100。
因为两个线程初次调用ThreadLocal的get方法时,都是从initialValue方法返回值,而该值是同一个,都指向成员变量bean,所以就造成了多个线程之间的变量共享了。
所以正确使用ThreadLocal真的很重要。
至此,ThreadLocal的存储模型基本就摸清楚了。我们在看一下内存泄露的那些事儿。
若你喜欢本文,可以分享给身边的朋友,或者关注我,谢谢!!!
我。