ThreadLocalMap.key到期之'探测是清理'+'启发式清理'流程
1. ThreadLocalMap.key到期的两种清理方式
上文中:ThreadLocal内存泄露问题 - lihewei - 博客园 (cnblogs.com) 我们提到ThreadLocalMap
的key
会因为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 启发式清理
启发式清理需要接收两个参数:
- 探测式清理后返回的数字下标
- 数组总长度
根据源码可以看出,启动式清理会从传入的下标 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的到期清理方式
- set() 方法中,遇到key=null的情况会触发一轮 探测式清理 流程
- set() 方法最后会执行一次 启发式清理 流程
- rehash() 方法中会调用一次 探测式清理 流程
- get() 方法中 遇到key过期的时候会触发一次 探测式清理 流程
- 启发式清理流程中遇到key=null的情况也会触发一次 探测式清理 流程
在ThreadLocal的源码中好多地方都用到了探测式清理 + 启发式清理,感兴趣的话可以阅读一下我的另一篇博客:ThreadLocal核心操作set/get/hash/扩容机制的原理及源码分析 - lihewei - 博客园 (cnblogs.com),里面详细介绍了 ThreadLocalMap的set()、rehash()、resize()、扩容机制等原理讲解及源码,一看就懂