ThreadLocal的内存泄露真的存在?(第一话)

共 8067字,需浏览 17分钟

 ·

2021-08-24 23:08

内存泄露?


ThreadLocal的前世今生

ThreadLocal这个对象相信大家并不陌生,但是这玩意有人说会造成内存泄露(感觉很严重的样子),听到这,怕是有些经验不足的童鞋会直接放弃使用它了。那么基于此,我们就研究一下,ThreadLocal到底是个什么东西,有什么用?又为什么会有人说他会造成内存泄露呢?真的有内存泄露吗?你真正的会用ThreadLocal吗?

如果你对上面的一连串发问,回答时带着好像、似乎、差不多的词汇,建议你看看本文。


01

ThreadLocal是什么?


6b23bdea0f61d8e510d1537700ef4712.webp

ThreadLocal就是一个java类,是jdk【java.lang】包下的一个很普通的类。他有一个非常重要的静态子类 ThreadLocalMap,该子类也有一个静态子类叫Entry,可以说ThreadLocal基本就是靠这哥俩,完成他的功能。


02

ThreadLocal有什么用?


A

●  线程级别的变量存储,成为线程的一个全局变量,从而可以让变量在各个方法中均可使用而不用传递参数


B

● 在高并发 多线程环境,实现不同线程之间的数据隔离


很多人说ThreadLocal可以让线程拥有一个共享变量的独立副本,且不同线程间修改其自己的副本,不会影响其他线程。听起来就像是ThreadLocal可以给每个线程拷贝该变量的副本,像Object.clone()一样。经过查看源码,私以为线程间的数据隔离这种说法比较靠谱。实际上,错误的使用ThreadLocal,不理解它的实现机制的话,往往会造成变量的共享。一个线程修改变量,导致其他线程的变量的值也被修改了。

4c409a180f21464563e8a93da00e883d.webp


所以在工作中,用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方法输出:null100

如此可以实现变量的跨方法调用,通常在非常复杂的业务逻辑中,在A方法中的变量要在G方法中使用,但是A-G之间的方法又不用这个变量,如果将该变量层层传递,就会显得过于累赘。使用ThreadLocal就会简单的多,看起来Bean变量就变成了main线程的全局变量,只要调用ThreadLocal的set方法以后,在main线程的其他地方就都可以使用了。这是ThreadLocal的其中一个用法。ThreadLocal不一定非要定义为成员变量,也可以在方法中定义。


4c409a180f21464563e8a93da00e883d.webp


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的弱引用问题待会再讲。

c9a4f696de11e63e8d0ef07055ba40f1.webp

类的关系图

326217b45474092508a645ac8af3c2e7.webp

为了便于理解,对象引用的关系如图(不代表实际内存位置)

我们的new出的Bean对象最终存到了Entry对象中的value属性中。


04

ThreadLocalMap的数据结构


所以ThreadLocal的get方法初次调用时,Entry[] table数据结构是这样的:

31390447fc3e4d2bb609429c5a8623dd.webp

当我们再调用了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了值,那么数据结构将如下图所示:

c53d9a665a60356b278cd187b186e10d.webp

当线程的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真的很重要。

bdbacece2aec831dd1260cebe5afe958.webp

至此,ThreadLocal的存储模型基本就摸清楚了。我们在看一下内存泄露的那些事儿。

ThreadLocal的内存泄露真的存在?(第二话)

点击链接即可跳转


若你喜欢本文,可以分享给身边的朋友,或者关注我,谢谢!!!

我。


浏览 36
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报