ThreadLocalMap的开放寻址法带来的问题思考

124 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

ThreadLocalMap的开放寻址法带来的问题

在ThreadLocalMap中解决hash冲突使用的开放寻址,意味着一旦出现hash冲突就要一直向后寻址位置,直到找到位置为null(entry为null)的地方为之。

set()分析

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(i, len)就是开放寻址方法的体系:

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

从该方法可以看出一次对角标i进行递增,当到达数组尾部时,重置为0。

replaceStaleEntry

这个方法比较有意义。它主要做了两件事:

  1. 找到当前元素应该存储的下标,并存入map中
  2. 清除map中的脏数据(Entry不为空但是key为空)
如何找到某一个key实际存储的位置。

当某一个key由于出现hash冲突时,需要从该下标开始向后循环找到自己可以存储的位置;由于ThreadLocalMap存在扩容机制,所以必定存在整个数组中Entry为null的位置。

扩容机制大小设置
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();


假设数组长度为8时,在某个元素A(hash值为0)存储之间,整个数组如下图所示:

image.png 根据上述规则,那么元素A就需要存放在index为3的位置上了。存储的效果图如下:

image.png 现在index为1的元素b调用了remove方法。该位置重置为null了。如果此时不把A移动到index为1上来,将会出现什么样的情况:按照不移动的情况分析下get()方法:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

假设expungeStaleEntry()此处也没有调用,此时i=0,但是key不相同,那么久需要向后查找,但是index=1位置处的元素不存在,此时需要终止循环,最终返回null; 这与我们的预期不符。 所以进入核心方法expungeStaleEntry()分析流程。

expungeStaleEntry 归位

该方法实际上做了2件事:

  1. 清除脏数据(key被回收,但是Entry不为null)
  2. 复位: 将原本不在此处的元素尽量向实际存储的位置靠近。
清除元素:
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

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--;
    } 

前者清除staleSlot处的元素,后者时再遍历过程中发现脏数据时进行移除,并对size进行递减。

移形换位:
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
    tab[i] = null;

    // Unlike Knuth 6.4 Algorithm R, we must scan until
    // null because multiple entries could have been stale.
    while (tab[h] != null)
        h = nextIndex(h, len);
    tab[h] = e;
}

首先根据当前key的hash值计算出预计存储的index为h,但实际存储下标为i,如果两者相等就无需操作;否则就从预期位置h一次向后循环查找可以存储的位置,直到找到w可存储的位置为止(由于存储扩容,所以必定存在空位置),并将实际存储位置的元素置为null。

image.png

总结:

时间有限,ThreaLocal中存在很多小的技巧值得我们去学习,当然光学不练假把式。加油吧!!!