ThreadLocal在执行set()方法的时候,实际执行set()逻辑的是其内部类ThreadLocalMap。
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
通过nextIndex()不断获取table上得槽位,直到遇到第一个为null的地方,此处也将是存放具体entry的位置,在线性探测法的不断冲突中,如果遇到非空entry中的key为null,可以表明key的弱引用已经被回收,但是由于线程仍未结束生命周期被回收而导致该entry仍未从table中被回收,那么则会在这里尝试通过replaceStaleEntry()方法,将null key的entry回收掉并set相应的值。
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
在replaceStaleEntry()方法中,带有入参staleSlot为已经得知并准备替换的槽位,该方法重点
一共要完成两件事,找到一趟hash下来第一个出现的nul key位置开始回收,第二件事,将staleSlot位置的null key entry替换为目前想要set的entry,当然该key可能已经在table中,需要将其移动大该位置。
第一个部分,将在目标槽位不断向前扫描直到遇到空槽位,在这一趟扫描下,将记录该位置最早出现的null key位置,并记录为slotToExpunge。
slotToExpunge为接下来清理null key的起始位置槽位,一定是一趟线性探测下来最早出现null key的那个槽位。
在向前扫描完成后,将会从staleSlot开始,向后不断获取下一个位置直到遇到空槽位,期间可能会发生三种情况。
第一种,key存在的其他entry,不管,跳过。
第二种,key不存在的null key entry,如果是首次,更新其slotToExpunge,准备作为接下来回收的起始位置,如果第一部分向前扫描的时候已经存在,将不会修改。
第三种,则是找到了本来将要set的key,也就是该次操作将要把该key从现有的位置swap到staleSlot位置,并回收掉staleSlot原本位置上的null key。
在上述第三种情况未发生的情况下,将会新建一个entry发到staleSlot位置。
在完成staleSlot位置的放入后,将会通过expungeStaleEntry()方法从该趟hash中遇到的第一个null key的位置回收,并在之后停过cleanSomeSlots()方法进行一次启发式的回收。
先看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;
}
这个方法主要做了两件事,一件事是从hash下某个值的一轮线性探测的首个null key空键开始向后不断回收,第二件事就是在这趟中的正常键将会进行rehash以便将其移至前序被回收的空槽位上。
在完成一趟hash下的空键回收和rehash之后,将会再次通过cleanSomeSlots()方法进行一次启发式的扫描回收。
private boolean cleanSomeSlots(int i, int 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) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
此处的遍历,将会在前一个方法中的最后一个key的下一个hash位置进行扫描并进行垃圾回收,理想情况下的执行次数是log2(散列表长度)次,当遇到null key空键时将会重置扫描次数,这里的次数实现选择是介于不扫描和全表遍历之间的一种选择,尽可能在保证效率的前提下所往后进行扫描。
在上述的整个流程走完后,回顾下最一开始的set()方法最后。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
当最后散列表中的个数大于负载个数后,将会调用rehash()方法进行扩容rehash分配。
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
在rehash()方法中,共分为两步expungeStaleEntries()方法将对散列表中的所有槽位进行一次遍历,对所有空键调用上述的expungeStaleEntryEntry()方法进行回收。
在回收之后,再次判断是否需要扩容,如果仍旧个数大于负载数量,将会resize(),将会通过resize()进行扩容,重新根据新的长度进行一次散列表的重分配。