ThreadLocal 是什么
作用
ThreadLocal
用于存储线程间的公有变量
数据结构
内存泄露?
要解释这个问题之前,须要先看 JAVA
对象中的 强援用、软援用、弱援用、虚援用
对象的四种援用类型
- 强援用
new
或通过反射创立进去的对象被称为强援用,只有强援用还存在,就不会被垃圾回收 - 软援用
应用SoftReference
润饰的对象被称为软援用,当内存不足时,软援用对象会被回收 - 弱援用
应用WeakReference
润饰的对象被称为弱援用,当对象只有弱援用时,GC 时,该对象会被回收 - 虚援用
应用PhantomReference
润饰的对象被称为虚援用,当对象被回收时会收到零碎告诉
WeakReference 案例介绍
public class WeakReferenceObj extends WeakReference<Object> {public WeakReferenceObj(Object referent) {super(referent);
}
public static void main(String[] args) {WeakReferenceObj weak = new WeakReferenceObj(new Object());
int i = 0;
while(true){if((weak.get()) != null){
i++;
System.out.println("Object is alive for"+i+"loops -"+weak);
}else{System.out.println("Object has been collected.");
break;
}
}
}
}
当以上程序运行了一段时间后,WeakReference
指向的对象就会只因被弱援用援用,而将对象回收
若将上诉代码革新为上面的代码
public class WeakReferenceObj extends WeakReference<Object> {public WeakReferenceObj(Object referent) {super(referent);
}
public static void main(String[] args) {Object o = new Object();
WeakReferenceObj weak = new WeakReferenceObj(o);
int i = 0;
while(true){System.out.println(o);
if((weak.get()) != null){
i++;
System.out.println("Object is alive for"+i+"loops -"+weak);
}else{System.out.println("Object has been collected.");
break;
}
}
}
}
你会发现,不论运行多久,弱援用指向的对象都不会被回收。因为此时的 o
还被一个强援用指向。即 打印流。
ThreadLocal 中的内存泄露
ThreadLocal
做为弱援用存在于 ThreadLocalMap key
中。因为是弱援用,当 ThreadLocal
只被弱援用指向时,在触发 GC
后,ThreadLocal
会被回收,即 ThreadLocalMap key
会为 null
。value
被 ThreadLocalMap
强援用指向,导致 value
无奈被回收。ThreadLocalMap
又是 Thread
的一个属性,因而除非 Thread
销毁,ThreadLocalMap
才会被开释,这样一来,Entry 不为 null ,key = null, value 又有值
(占着茅坑不拉屎),ThreadLocalMap
如果没有无效的 清理 Entry 不为 null, key = null
的机制,那么就会因为 value
无奈被回收,从而导致内存泄露。
ThreadLocal 清理机制
在 ThreadLocal
内存泄露的剖析中,咱们晓得,如果 ThreadLocal
没有无效的清理机制,那么必然会导致内存泄露。那么接下来将介绍 ThreadLocal
中 2
种清理机制,避免内存泄露
探测式清理
代码的逻辑在:ThreadLocalMap.expungeStaleEntry
从 key = null
的地位向前清理,而后遍历 ThreadLocalMap
直到 Entry != null
。如果遇到 key = null
则将 Entry、value
设置为 null
,如果 key != null
, 则从新 hash
从新将该 Entry
放入 ThreadLocalMap
中。
ThreadLocal
的 get()
, set(T t)
,从何处开始清理不大一样,然而最终都是调用 expungeStaleEntry
办法,进行清理。
get()
清理点为:在从 x
下标 获取不到对应 key
的 value
时,会从 x
下标开始清理
set(T t)
清理点为:如果在设置值时,发现在 x
下标 key = null
。则会从 x
往前查找 key = null
,直到 Entry = null
,如果查找到,x
会被赋予方才元素的下标。最初再从 x
处开始清理。
启发式清理
代码的逻辑在:ThreadLocalMap.cleanSomeSlots
从 i
地位开始,直到以后 ThreadLocalMap
中 Entry
个数 n >>> 1 != 0
。
如果 Entry != null
,但 key = null
, 会调用 expungeStaleEntry
进行清理。
如何预防
应用结束后,调用 remove
办法,进行清理。
论断
get、set
办法在外部均会对过期 key
进行清理。然而为了以防万一,在应用结束后,还须要手动调用 remove
办法进行清理
ThreadLocal Hash 算法
ThreadLocal 中有个属性 HASH_INCREMENT = 0x61c88647
,它被称为 黄金分割数,hash
增量为该数字,因而,产生的 hash
数值十分的平均。
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) {for (int i = 0; i < 16; i++) {
int hash = HASH_INCREMENT * i + HASH_INCREMENT;
System.out.print(hash & (16 - 1));
System.out.print(",");
}
}
生成的后果如下:
7,14,5,12,3,10,1,8,15,6,13,4,11,2,9,0,
ThreadLocal Hash 抵触
ThreadLocal Hash 抵触应用的是 线性探测再散列的凋谢寻址法 。
所谓线性探测算法如下:从以后发生冲突的地位,往后查找,直到找到一个 null 的地位插入。
扩容
当进行 set
后,会执行 cleanSomeSlots
如果有清理元素,且数组大小达到数组扩容阈值 threshold
(len * 2 / 3
)则会进行探测式清理。如果清理结束后,数组大小大于 treshold * 3 / 4
则进行扩容。
扩容时,数组变为原来的 2
倍,且将整个 ThreadLocalMap
的 key
从新 hash
放入 table
中
灵魂拷问,为什么 ThreadLocalMap key 是弱援用?
key 是强援用
ThreadLocalMap
的生命周期与 Thread
统一。如果 Thread
存活太久,增加了十分多的 ThreadLocal
。此时若在代码中将 ThreadLocal
设置为 null
,理当被回收。然而,因为 ThreadLocalMap
还存在 ThreadLocal
的强援用,而导致无奈被回收,从而导致内存泄露。并且在代码外面,很难判断 ThreadLocal
在别的中央还有没有援用。
key 是弱援用
ThreadLocal
是弱援用,代码中被设置为 null
后,因为只存在弱援用,所以,在 GC
后会被失常回收。然而 key = null
也会存在 value
内存泄露。尽管 value
会存在内存泄露,然而能够通过判断 key = null
来判断,ThreadLocal
已没有其余援用。
论断
集体认为最外围的起因是:ThreadLocalMap
的生命周期与 Thread
统一。太过难于判断ThreadLocal
只在 ThreadLocalMap
中有援用。因而设置为弱援用,让 GC 回收 ThreadLocal
后,用 null
来判断
参考
面试官:据说你看过 ThreadLocal 源码?我来瞅瞅?