ThreadLocalMap.key到期之'探测是清理'+'启发式清理'流程

1. ThreadLocalMap.key到期的两种清理方式

上文中:ThreadLocal内存泄露问题 - lihewei - 博客园 (cnblogs.com) 我们提到ThreadLocalMapkey会因为GC导致过期,在ThreadLocalMap中有数据清理方式,分别是:

  • 探测式清理(源码中:expungeStaleEntry() 方法 )
  • 启发式清理(源码中:cleanSomeSlots() 方法 )

1.1 探测式清理

​ 探测式清理:也就是源码中expungeStaleEntry()方法,遍历散列数组,从开始位置(hash得到的位置)向后探测清理过期数据,将过期数据的 Entry 设置为 null ,沿途中碰到未过期的数据则将此数据rehash后重新在 table 数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的 Entry=null 的桶中(顺序往后延),使 rehash 后的 Entry 数据距离正确的桶的位置更近一些。

说人话就是:从当前节点开始遍历数组,key==null的将entry置为null,key!=null的对当前元素的key重新hash分配位置,若重新分配的位置上有元素就往后顺延。

expungeStaleEntry()源码:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

1.2 启发式清理

启发式清理需要接收两个参数:

  1. 探测式清理后返回的数字下标
  2. 数组总长度

根据源码可以看出,启动式清理会从传入的下标 i 处,向后遍历。如果发现过期的Entry则再次触发探测式清理,并重置 n。这个n是用来控制 do while 循环的跳出条件。如果遍历过程中,连续 m 次没有发现过期的Entry,就可以认为数组中已经没有过期Entry了。
这个 m 的计算是 n >>>= 1 ,你也可以理解成是数组长度的2的几次幂。
例如:数组长度是16,那么24=16,也就是连续4次没有过期Entry,即 m = logn/log2(n为数组长度)

说人话就是: 从当前节点开始,进行do-while循环检查清理过期key,结束条件是连续n次未发现过期key就跳出循环,n是经过位运算计算得出的,可以简单理解为数组长度的2的多少次幂 次,例如:

cleanSomeSlots()源码:

private boolean cleanSomeSlots(int i, int n) {  //探测式清理后返回的数字下标,这里至少保证了Hash冲突的下标至探测式清理后返回的下标这个区间无过期的Entry, n 数组总长度
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {  // 如果发现过期的Entry就在执行一次探测性清理
            n = len;  //重置n
            removed = true;
            i = expungeStaleEntry(i);   //探测性清理
        }
    } while ( (n >>>= 1) != 0);  // 循环条件: m = logn/log2(n为数组长度)
    return removed;
}

2. 哪些地方会触发这两种key的到期清理方式

  1. set() 方法中,遇到key=null的情况会触发一轮 探测式清理 流程
  2. set() 方法最后会执行一次 启发式清理 流程
  3. rehash() 方法中会调用一次 探测式清理 流程
  4. get() 方法中 遇到key过期的时候会触发一次 探测式清理 流程
  5. 启发式清理流程中遇到key=null的情况也会触发一次 探测式清理 流程

在ThreadLocal的源码中好多地方都用到了探测式清理 + 启发式清理,感兴趣的话可以阅读一下我的另一篇博客:ThreadLocal核心操作set/get/hash/扩容机制的原理及源码分析 - lihewei - 博客园 (cnblogs.com),里面详细介绍了 ThreadLocalMap的set()、rehash()、resize()、扩容机制等原理讲解及源码,一看就懂

posted @ 2023-03-14 16:22  lihewei  阅读(1903)  评论(0编辑  收藏  举报
-->