重温 JAVA -- ThreadLocal 终
文章目录
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
来判断
参考
推荐阅读
-
Java面试题必备知识之ThreadLocal
-
java ThreadLocal使用案例详解
-
面试字节跳动三轮凉凉,内推4面终拿下抖音offer(Java后台研发)
-
java中ThreadLocal取不到值的两种原因
-
Java并发神器——ThreadLocal
-
Java代码质量改进之:使用ThreadLocal维护线程内部变量
-
java线程本地变量ThreadLocal详解
-
重温 Thinking in Java - 2 Method Overloading
-
Java单例模式的不同写法(懒汉式、饿汉式、双检锁、静态内部类、ThreadLocal、枚举)
-
JAVA并发编程(六):线程本地变量ThreadLocal与TransmittableThreadLocal