ThreadLocal知识点小结
什么是ThreadLocal?
ThreadLocal为每个线程提供线程内部的变量副本,保证在多线程的环境下每个线程维护的变量独立于其他线程。
什么场景会用到它?
1.在每个线程都需要独立的线程变量的场景(对于一些线程不安全的变量,比如SimpleDataFormat类,多线程环境下会有线程安全问题,如果放到ThreadLocal里来维护,可以解决线程安全的问题)
2.如果还有一些业务场景,要求变量在线程间需要隔离,在方法或类之间需要共享。可以考虑使用ThreadLocal
上面说了ThreadLocal是干什么用的,下面来了解下ThreadLocal怎么来使用,以及从源码级别来分析下它的原理。
每个Thread类都有一个ThreadLocalMap变量,ThreadLocalMap内部维护了多个Entry节点,即Entry数组。每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的要存储的范型变量。
主要方法:
get() 方法:获取当前线程维护的变量副本
首先是先获取当前线程的ThreadLocalMap,然后再根据当前的ThreadLocal获取key对应的entry里的维护的变量副本。如果之前没有set过,就先初始化一个ThreadLocalMap出来,并返回空。所以可以看到这里是“懒汉”模式。源码如下:
set()方法:为当前线程设置一个新的变量副本
首先是先获取当前线程对应的ThreadLocalMap,如果之前set过,这次直接进行覆盖,如果没有set过,先实例化一个ThreadLocalMap出来再set
remove()方法:
删除这个线程维护的变量的值。一般在逻辑处理结束后进行手动remove下,防止内存泄漏。不过只要线程的生命周期结束了,线程对应的线程变量会自动进行回收。
总结:
其实一句话就是Thread里维护了ThreadLocalMap,ThreadLocalMap里维护了Entry数组,每一个Entry数组存的都是以ThreadLocal对象为key,变量为value的键值对。
一点思考:
1.都是为了线程安全,它和synchronized有什么区别和联系?
synchronized通过对对象头加锁的方式控制多线程对共享变量的访问,保证加锁的代码任何时候的某个时刻都只有一个线程在执行,所以共享变量永远只有一份。例如统计某个网站访问的次数,通过synchronized来解决,但是ThreadLocal不行的。而ThreadLocal是为每个线程都设置一个独立的变量,各个线程之间的变量是互相独立的,互不影响。比如spring mvc收到用户的请求,会通过拦截器把当前用户id放到threadlocal里,后边的操作可以直接拿来用。可以说ThreadLocal和synchronized没什么联系,解决的场景不同。
2.ThreadLocal的数据存储在JVM的哪个区呢?
ThreadLocal对象也是对象,是对象就存在堆里。只不过JVM通过一些特殊的设置,变成了线程可见
3.ThreadLocal为什么会导致内部泄漏呢?
也许有小伙伴该产生疑问了,上面不是说了线程生命周期结束了,线程对应的对象都会回收,为什么还会产生内存泄漏呢。先看下存储结构:
Entry继承的父类是弱引用,k是通过super方法调用父类的方法,所以k依旧是弱引用,而value是强引用。因为弱引用本身的特性,只要jvm里空间不够了,弱引用的对象就有可能被回收。而value是强引用,只要垃圾回收算法标示到是可达的,就不会被回收,就会形成:
Thread->ThreadLocalMap->Entry(key为null)->value
弱引用的特点:如果这个对象只被弱引用关联,没有任何强引用关联,那么这个对象就可以被GC回收掉。弱引用不会阻止GC回收。
但是可能有小伙伴还会有疑问了。上面不是说了线程生命周期结束了,线程对应的对象都会回收,好像和弱引用或者强引用都没啥关系。但是还有一种场景,就是线程池的场景,为了复减少线程反复创建和销毁的代价,这种场景下核心线程会一直长驻,这种场景下核心线程就不会销毁,对应的ThreadLocalMap的数据也会一直存在,由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,时间久了必定OOM。
所以为了安全角度考虑,无论是否使用线程池,都建议在执行完后都手动remove下,养成好习惯。
4.还有哪些开源框架用到了ThreadLocal?
例如Spring框架的RequestContextHolder就用到了ThreadLocal。永远记录用户登陆的信息,既保持了线程里信息独立,又可以把用户的信息一直携带下去,方便后续业务逻辑的处理。