ThreadLocal 是什么

作用

ThreadLocal 用于存储线程间的私有变量

数据结构

image.png

内存泄露?

要解释这个问题之前,需要先看 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 指向的对象就会只因被弱引用引用,而将对象回收
image.png

若将上诉代码改造为下面的代码

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 会为 nullvalueThreadLocalMap 强引用指向,导致 value 无法被回收。ThreadLocalMap 又是 Thread 的一个属性,因此除非 Thread 销毁,ThreadLocalMap 才会被释放,这样一来,Entry 不为 null ,key = null, value 又有值(占着茅坑不拉屎),ThreadLocalMap 如果没有有效的 清理 Entry 不为 null, key = null 的机制,那么就会因为 value 无法被回收,从而导致内存泄露。

ThreadLocal 清理机制

ThreadLocal 内存泄露的分析中,我们知道,如果 ThreadLocal 没有有效的清理机制,那么必然会导致内存泄露。那么接下来将介绍 ThreadLocal2 种清理机制,防止内存泄露

探测式清理

代码的逻辑在:ThreadLocalMap.expungeStaleEntry
key = null 的位置向前清理,然后遍历 ThreadLocalMap 直到 Entry != null。如果遇到 key = null 则将 Entry、value 设置为 null,如果 key != null, 则重新 hash 重新将该 Entry 放入 ThreadLocalMap 中。

ThreadLocalget(), set(T t),从何处开始清理不大一样,但是最终都是调用 expungeStaleEntry 方法,进行清理。

get()

清理点为:在从 x 下标 获取不到对应 keyvalue 时,会从 x 下标开始清理

set(T t)

清理点为:如果在设置值时,发现在 x 下标 key = null。则会从 x 往前查找 key = null,直到 Entry = null,如果查找到, x 会被赋予刚才元素的下标。最后再从 x 处开始清理。

启发式清理

代码的逻辑在:ThreadLocalMap.cleanSomeSlots

i 位置开始,直到当前 ThreadLocalMapEntry 个数 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 如果没有清理元素,且数组大小达到数组扩容阈值 thresholdlen * 2 / 3)则会进行探测式清理。如果清理完毕后,数组大小大于 treshold * 3 / 4 则进行扩容。

扩容时,数组变为原来的 2 倍,且将整个 ThreadLocalMapkey 重新 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源码?我来瞅瞅?


心无私天地宽
513 声望22 粉丝