持续创作,加速成长!这是我参与「掘金日新计划 · 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
这个方法比较有意义。它主要做了两件事:
- 找到当前元素应该存储的下标,并存入map中
- 清除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)存储之间,整个数组如下图所示:
根据上述规则,那么元素A就需要存放在index为3的位置上了。存储的效果图如下:
现在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件事:
- 清除脏数据(key被回收,但是Entry不为null)
- 复位: 将原本不在此处的元素尽量向实际存储的位置靠近。
清除元素:
// 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。
总结:
时间有限,ThreaLocal中存在很多小的技巧值得我们去学习,当然光学不练假把式。加油吧!!!