浅谈ThreadLocal

ThreadLocal

保存每个线程各自的局部变量,线程隔离。即,在多线程情况下,线程的局部变量不能被共享访问。

先来看看下面这段代码:

public class Main {
	//ThreadLocal
    public static ThreadLocal<Person> tl=new ThreadLocal<Person>();
    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //向Threadlocal中放入一个对象
                tl.set(new Person());
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //从Threadlocal中获取之前放入的对象
                System.out.println(tl.get());
            }
        }).start();
    }
  }

试猜想第二个线程是否能够获取到第一个线程放入的对象?
在这里插入图片描述
事实证明是不能的。浅显地理解:ThreadLocal相当于一个容器,a线程放入了一个对象,b线程再去获取这个对象,但b获取不到,说明该容器中有一个机制将a、b线程给区分开了。


那它们到底是分隔开的?我们先来看看源码怎么说:

浅谈源码

set

先看第一个线程中,ThreadLocal对象调用的set()方法:
在这里插入图片描述
可以看到,它是先去获取了当前线程(注意!),然后通过拿着当前线程去调用getmap,获得了一个ThreadLocalMap对象,看起来是一个map结构。
那就看看这getmap又是何方神圣?
在这里插入图片描述
threadLocals是一个ThreadLocalMap类型的引用,注意:它是在Thread类下的,说明每一个Thread对象都拥有一个自己的ThreadLocalMap。
在这里插入图片描述
我们再来看看ThreadLocalMap:
在这里插入图片描述
ThreadLocalMap中保存着一个个的Entry,这个Entry继承自WeakReference,WeakReference是弱引用。Entry的构造方法中有一个super(k),这个K是ThreadLocal。说明K,也就是这个ThreadLocal它是被弱引用指向的。
补充:

弱引用:在GC时,不管内存是否足够,都会被回收。

关于四种引用

为什么用弱引用?强引用不可以吗?

这个问题稍后解释,因为还牵扯到另一个很重要的点。

先关注ThreadLocalMap,虽然还不清楚它的数据结构,但通过大致结构可以看出它是以map的形式进行存储的,存储很多个以ThreadLocal 为K、以变量的副本为V的Entry。


再回过头去看看set的源码:
如果通过当前线程获取的map是空的,就创建一个map来保存当前线程和它的变量副本;
如果不为空,就set一个(this,value)的键值对,这个this是谁?
ThreadLocal !忘了吧,前面的代码里是谁调用的set()啊?
在这里插入图片描述

那ThreadLocalMap和ThreadLocal是个什么关系?

结构层面:ThreadLocalMap是ThreadLocal的一个内部类。
理解上:
还记得前面我们追溯到threadLocals时得知:每一个线程都有自己的ThreadLocalMap,也就是说每一个Thread对象中都有一个threadLocals引用,这个引用指向一个ThreadLocalMap,这个map中管理了一个个的Entry(K,V)。这个K就是ThreadLocal,说白了就是ThreadLocal将自己作为一个K保持在map中,ThreadLocalMap就是ThreadLocal的一个存取值的私人空间。


好了,到这儿咱们就看完了ThreadLocal的set方法,那get呢?咱接着看:
其实你可能大致想到了上面例子中第二个线程为什么获取不到第一线程放进去的对象了。

set源码中的第一行:先获取了当前对象!

get

看get源码:
在这里插入图片描述
它第一件事也是先获取当前线程,再拿着这个去获取当前线程自己的ThreadLocalMap。
根据ThreadLocal去获取对应的Entry。

  ThreadLocalMap.Entry e = map.getEntry(this);

获取到了就将这个Entry中保存的变量副本Value返回。
获取不到,说明map中没有这个ThreadLocal对应的Entry,可以看到获取不到就调用了setInitialValue()这个方法。
在这里插入图片描述
这个方法做两件事:
1.通过initialValue()得到一个value的初始值
在这里插入图片描述
2.给map中设置value值。

  • 获取当前线程,拿着去找map。
  • 获取到map后将ThreadLocal和上面初始化的value给它设置进去 。
  • 获取不到就创建一个map,最终返回value。
    这个value是initialValue()得到的初始值,value为null。

到这儿,get咱也大概知道咋回事了,兜了这么大个圈子,就是告诉咱们:虽然你和我都是对ThreadLocal操作,但你的是你的,我的是我的,咱俩谁也不用干扰谁。


那这个弱引用又是什么情况?

如果了解垃圾回收机制,就会知道如果一直创建对象而不清理内存,就会导致内存泄露(后果很严重!)。

内存泄漏

首先,需要明确的是这里的内存泄漏不是因为使用弱引用而造成的。

使用强引用指向ThreadLocal

如果使用强引用指向ThreadLocal,那gc时,即使它已经没用了,但永远不会被回收,除非这个线程结束。但实际业务中有可能这个线程会运行很久很久,那最终就可能导致内存泄漏了。

为什么用弱引用?

因为弱引用是在gc时,不管内存够不够,都会被回收。

那用弱引用为什么还会内存泄漏呢?

因为map中只有K是被弱引用指向的,value怎么办?
ThreadLocalMap中只持有ThreadLocal的弱引用,不能指向ThreadLocal实例,当指向ThreadLocal的弱引用被回收后,K=null了,但value还存在,却再也没有办法通过K去找到这个value了。
不处理的话,内存中就会遗留很多个这样(null,V)的entry,最终导致内存泄漏。

所以说,内存泄漏和使用了什么引用没什么关系。

造成内存泄漏的原因是:
因为每个线程都拥有自己的ThreadLocalMap,并且ThreadLocalMap的生命周期和Thread一样长,那map中的无用的Entry不能被及时清理掉,且不断产生,就会导致内存泄漏。

应该如何解决?

需要手动调用remove()方法将整个entry都删掉。这个remove()内部是调用了ThreadLocalMap的remove()方法。
在这里插入图片描述


可是,既然最后要手动调用remove()删除整个Entry,那为什么还在这强调用弱引用呢?

相对而言,如果忘记手动remove,弱引用造成的代价会比强引用小。基于弱引用的特点,在gc时会清理一部分key,同时在ThreadLocalMap的remove/get/set方法里也添加了清除无效key-value的逻辑,避免了引起严重的内存泄漏。

参考文章


关于ThreadLocal,我只是学习到了冰山一角,还有更多的问题需要去探究,慢慢来吧。

由于个人水平有限,若有不当之处,还请批评指正,谢谢!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值