用通俗易懂的大白话讲明白ThreadLocal

一、Thread、ThreadLocal、ThreadLocalMap

首先来说一下Thread、ThreadLocal、ThreadLocalMap之间的关系

对ThreadLocal稍微有点了解的应该都知道,一说到ThreadLocal,就会涉及到另外两个东西:Thread和ThreadLocalMap

那它们之间是什么关系呢?

简单来说:线程类Thread类 里有一个全局的变量叫 threadLocals,这个变量它的类型是ThreadLocal.ThreadLocalMap,ThreadLocalMap呢,它是ThreadLocal类里的一个内部类,它虽然没实现Map接口,但它和Map非常类似,里边也是放键值对儿,它里边放的键值对的key就是当前的ThreadLocal对象,里边放的键值对的value 就是你想放的那个数据

再来强调一下ThreadLocalMap:它是Thread类里的一个全局变量属性,它是归Thread类所有的。它的引用在Thread类里,这也证实了一个问题:ThreadLocalMap类内部为什么有Entry数组,而不是Entry对象?
因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocal,ThreadLocalMap在一个线程里就一个,因为再说一次,ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象为key的数据

大概关系如下图所示:

二、搞清楚ThreadLocal里的set方法

我个人觉得啊,ThreadLocal 里set方法的名字可能会让人有点误解,让人觉得这个set方法是往ThreadLocal里set数据。那set真的是往ThreadLocal里塞数据吗?

其实,这个set,它不是往ThreadLocal里塞东西(首先明白这一点非常重要)。
看它的源码,它是先拿到当前线程里的那个map 也就是 threadLocals,threadLocals它的类型是ThreadLocal.ThreadLocalMap,刚才说了ThreadLocalMap是ThreadLocal类里的一个内部类,它是个和Map非常类似的东西,里边也是放键值对
拿到这个“map”后,往这个map里塞键值对儿进去,塞进去的键就是 当前的ThreadLocal对象,值就是你想set的value

看下它的源码

 为了便于理解,下面我把源码贴出来,每一行都加上了注释


public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程对应的ThreadLocalMap实例,
    //注意这里是将t传进去了,t是当前线程,就是说ThreadLocalMap是在线程里持有的引用。
    ThreadLocalMap map = getMap(t);
    //若当前线程有对应的ThreadLocalMap实例,
    //则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
    if (map != null)
        map.set(this, value);
    else
        //若当前线程没有对应的ThreadLocalMap实例,
        //则创建ThreadLocalMap,并将此线程与之绑定
        createMap(t, value);
}

三、ThreadLocal里经常提到的内存泄漏

首先需要注意的一点是:咱们讨论内存泄漏都是在线程不销毁不终止的情况下的,如果线程都终止了,那还有个鬼的内存泄漏问题啊

ThreadLocalMap 里的 Entry 是继承 WeakReference 的,也就是弱引用
看源码里,Entry的构造函数里调了super(k),也就是调用了父类 WeakReference 的构造,而且在调父类的构造的时候是把key传了进去
也就是说,它是把key做成了一个弱引用的对象

如图可以看下源码

那它把key做成了一个弱引用的对象的好处是啥呢?
大家知道,弱引用的一个特点就是:如果这个对象只被弱引用关联,没有任何强引用关联,那么GC时,这个对象一定会被GC回收掉。
这样做的好处就是,当栈中对这个ThreadLocal对象的引用断了以后,那就剩这个map中的key对这个 ThreadLocal 引用了,而这个key搞成弱引用的对象,就相当于此时这个ThreadLocal对象只被弱引用关联,那在发生垃圾回收时就会把这个ThreadLocal 对象回收掉。 

但是,反过来想,如果这个key不被搞成弱引用,而是普通的强引用的话,那就会出现:由于map的key对ThreadLocal对象有强引用而造成ThreadLocal对象迟迟不能被回收的现象,也就是key的内存泄露。

再看value,因为它只把key做了弱引用,value并没有去做特殊处理,所以value是个强引用

但是想了下也没问题的呀,因为线程终止了,我管你强引用还是弱引用,都会被GC掉的,因为引用链断了(jvm用的可达性分析法,线程终止了,GCRoot根节点就断了,下面的都会被回收),所以刚开始咱们就说了讨论内存泄漏都是在线程不销毁不终止的情况下的
所以咱们来讨论线程不终止的情况,比如线程池,线程池里的核心线程是不会销毁的,只要创建出来它会反复利用,生命周期不会结束掉,key是弱引用会被GC回收掉,但是value是强引用,不会回收,所以形成了如下场面:就是当这个ThreadLocal被回收了,此时map里就会出现key是null,但是value还在

Thread->ThreadLocalMap->Entry(key为null)->value

由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,造成内存泄漏,时间久了必定OOM。

所以把key做成弱引用的这种处理,只能保证key不内存泄漏;但是value还是可能会出现:明明没用了,但是却迟迟回收不掉的情况

那这种value迟迟回收不掉的情况怎么办呢?

解决方案ThreadLocal已经为我们想好了
ThreadLocal 的做法是:在任何一次调用它的set、get这些方法时,都会去看看这个map中有没有key为null的,有的话就把key为null的这些数据全部剔除掉。
但是,但是!这种做法能保证大部分情况下value不内存泄漏
但依然不能百分百保证内存不泄漏,比如 万一你就是一直迟迟不调用它的set、get方法呢
所以,如果想百分百保证内存不泄漏的话,还是养成良好的编程习惯,在用完了ThreadLocal后,就调用它的remove()方法,这样肯定能百分百保证内存不泄漏

四、线程池的情况下使用ThreadLocal

还有一个需要注意的地方就是
如果在线程池的情况下使用ThreadLocal的话,会出现一个问题:
由于线程池中的线程,那些核心线程是不会被销毁的,所以你这次从池中拿出的线程并不是新产生的线程,而是上次用完还回池里的线程
那就有可能出现一种情况:你这次拿出的Thread对象里的 threadLocals这个map,这个map里可能会有上次或者之前运行时存进去的一些数据,可能会出现各种各样的混淆
所以正确的做法应该是,线程用完后,还回池中做的第一件事就是把线程里的 threadLocals 清除掉

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嗑嗑嗑瓜子的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值