threadlocal的set()方法中的内存回收

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()进行扩容,重新根据新的长度进行一次散列表的重分配。

你可能感兴趣的:(jdk)