强引用、软引用、弱引用、虚引用
在了解threadLocal之前,有必要了解JAVA中的四种引用:
- 强引用:正常new出来对象就是强引用,当内存不够的时候,JVM宁可抛出异常,也不会回收强引用对象。
- 软引用(
SoftReference
):软引用生命周期比强引用低,在内存不够的时候,会进行回收软引用对象。软引用对象经常和引用队列ReferenceQueue
一起使用,在软引用所引用的对象被GC回收后,会把该引用加入到引用队列中。 - 弱引用(
WeakReference
):弱引用生命周期比软引用要短,在下一次GC的时候,扫描到它所管辖的区域存在这样的对象:一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向,
,不管当前内存是否够,该对象都会被回收。弱引用和软引用一样,也会经常和引用队列ReferenceQuene
一起使用,在弱引用所引用的对象被GC回收后,会把该引用加入到引用队列中。 - 虚引用(
PhantomReference
):又叫幻象引用,与软引用,弱引用不同,虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。它的唯一作用就是当其指向的对象将被回收时,自己被加入到引用队列,用作记录该引用指向的对象即将被销毁。
finallized方法: 当对象变成(GC Roots)不可达时(第一次回收),GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达(第二次回收),若不可达,则进行回收,否则,对象“复活”。因此,对于重写了finallized方法的对象,会出现两个垃圾回收周期,这两个周期之间可能相隔了很久(取决于finalized方法执行是否及时),所以可能会出现大部分堆被标记为垃圾却还没有被回收,出现内存溢出的错误。
使用虚引用,上述情况将引刃而解,当一个虚引用加入到引用队列时,你绝对没有办法得到一个销毁了的对象。因为这时候,对象已经从内存中销毁了。因为虚引用不能被用作让其指向的对象重生,所以其对象会在垃圾回收的第一个周期就将被清理掉。
ThreadLocal
通常情况下,线程中对全局变量赋值后,可以被任何一个线程访问并修改的。
而创建全局变量ThreadLocal
,通过ThreadLocal
全局变量传递局部变量,该局部变量只能被当前线程访问,而且可以在线程的上下文传递,其他线程则无法访问和修改。
1 | public class Test { |
实际上通过ThreadLocal
设置的值是放入了当前线程的一个ThreadLocalMap
实例中,所以只能在本线程中访问,其他线程无法访问。
ThreadLocal的实现原理
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例,value是真正需要存储的Object。
从set()方法的实现,理解ThreadLocal实现
1 | // jdk1.8 source code |
在调用set方法时
- 首先获取当前线程
Thread.currentThread()
- 利用当前线程获取一个
ThreadLocalMap
对象 - 判断map是否为空,若为空,创建这个
ThreadLocalMap
对象并设置值,不为空,则设置值。
getMap()
方法:
1 | ThreadLocalMap getMap(Thread t) { |
在Thread
类中,定义了两个属性,threadLocals
的初始化是在调用ThreadLocal
类中的getMap()
方法时完成的,当线程退出时,会将threadLocals
和inheritableThreadLocals
置为null。
1 | ThreadLocal.ThreadLocalMap threadLocals = null; // ThreadLocalMap对象 |
现在,完成了前两步,获取当前线程的ThreadLocalMap
对象。
ThreadLocalMap
是ThreadLocal
的静态内部类,是基于Entry数组的map。Entry
的key
是ThreadLocal
弱引用,目的是当线程退出时把threadLocal
实例置为null时,不再有强引用指向threadLocal
实例,不影响threadLocal
实例的垃圾回收。
1 | static class ThreadLocalMap { |
在threadlocal
的生命周期中,存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.
与上面的分析一致,Entry的key为弱引用,它的引用链是ThreadLocalRef -> ThreadLocal ---> key
,当栈中的ThreadLocalRef
与堆中的ThreadLocal
断开时,ThreadLocal
实例就会被垃圾回收。
value为强引用,它的引用链是CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry -> value
,只要当前线程没有关闭,CurrentThreadRef -> CurrentThread
的引用就不会断开,value就不会被垃圾回收。只有当前thread结束以后, CurrentThread
就不会存在栈中,强引用断开, CurrentThread, Map, value将全部被GC回收.
是否存在内存泄露?
上节提到当前线程没有退出,将会一直存在CurrentThread至value的引用链,即便将threadLocal手动设置为null也依然存在CurrentThread至value的引用链。这会给开发者产生一种内存泄露的错觉(错觉:value是通过threadLocal设置的,我明明将threadLocal设置为了null,为什么value还会占用内存?),尤其在使用线程池时更容易出现这样的错觉,因为线程池的线程结束后,会放回线程池中不销毁。
可以理解为:threadLocal没有内存泄露,泄露的是Entry。
JDK的优化
为了减缓这种错觉的产生,Java会在调用threadLocal实例的get、set方法且key为null时,清除Entry。以get方法为例:
1 | // threadlocal.get() |
不仅在调用get方法,在调用set、remove方法时,threadLocal为null时,也会最终调用到expungeStaleEntry()
方法 ,清除所有threadLocal为null时entry的强引用,这里不赘述了。
因此,正确的使用方式是,首先判断是否存在场景:threadLocal置为null?
如果存在,在调用完set、get后,记得调用remove方法显示的清除Entry的强引用。如果不存在,threadLocal一直在使用,没有被回收的必要,也不care脏读的情况,那更没必要去回收threadLocalMap中的Entry了。
脏读
示例如下,创建一个大小为8的线程池,向该线程池提交100次任务,因为使用的是线程池,线程不会被销毁,所以假设某一个线程写入了值,然后该线程处于空闲态,然后该线程再次读取时,读取到的是上次该线程运行时设置的值。
可能下面的例子很明显就看的出问题所在,但是当项目复杂时,在多处调用get,就比较容易出现这种问题。
不过这种情况也很容易避免,有两种方法:
- set、get成对出现,set在前、get在后
- 使用remove
1 | public class Test { |
Hash碰撞
在某个线程中,每new一个ThreadLocal实例,该线程的ThreadLocalMap
中就会新增的一个key,当ThreadLocal实例过多时,自然会出现hash碰撞。
和HashMap
的最大的不同在于,ThreadLocal.ThreadLocalMap
结构非常简单,没有next引用,也就是说ThreadLocalMap
中解决Hash冲突的方式并非链表/红黑树的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以在开发的过程中,要避免这一点,提高运行效率。
与synchronized的区别
ThreadLocal
用于处理线程内部上下文变量的传递,变量不会被其他线程访问,而synchronized
修饰的变量,只要其他线程获取了锁,就能访问、修改ThreadLocal
没有锁的机制,没有锁的开销