// 当前 key 的哈希,即在数组 table 中的位置
for (Entry e = tab[i];
e != null; // 循环直到碰到空 Entry
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // 更新 key 对应的值
e.value = value;
return;
}
if (k == null) { // 替代过期 entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 通过
key.threadLocalHashCode & (len-1)
计算出初始的哈希值
- 不断调用
nextIndex()
直到找到空 Entry
在第二步遍历过程中的每个元素,要处理两种情况:
(1). k == key
,说明当前 key 已存在,直接更新值即可,直接返回
(2). k == null
, 注意这里的前置条件是 entry != null
。说明遇到过期 Entry,直接替换
- 不属于 3 中的两种情况,则将参数中的键值对插入空 Entry 处
cleanSomeSlots()/rehash()
先来看看第三步中的第二种特殊情况。Entry
不为空,但其中的 key
为空,什么时候会发生这种情况呢?对,就是前面说到内存泄漏时提到的 过期 Entry。我们都知道 Entry 的 key 是弱引用的 ThreadLocal
,当外部没有它的任何强引用时,下次 GC 时就会将其回收。所以这时候的 Entry 理论上也是无效的了。
由于这里是在 set() 方法插入元素的过程中发现了过期 Entry,所以只要将要插入的 Entry 直接替换这个 key==null
的 Entry 就可以了,这就是 replaceStaleEntry()
的核心逻辑。
replaceStaleEntry()
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
// 向前找到第一个过期条目
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; // 记录前一个过期条目的位置
// Find either the key or trailing null slot of run, whichever occurs first
// 向后查找,直到找到 key 或者 空 Entry
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
// 如果在向后查找过程中发现 key 相同的 entry 就覆盖并且和过期 entry 进行交换
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 如果在查找过程中还未发现过期 entry,那么就以当前位置作为 cleanSomeSlots 的起点
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
// 如果向前未搜索到过期 entry,而在向后查找过程遇到过期 entry 的话,后面就以此时这个位置
// 作为起点执行 cleanSomeSlots
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 如果在查找过程中没有找到可以覆盖的 entry,则将新的 entry 插入在过期 entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
// 在上面的代码运行过程中,找到了其他的过期条目
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
看起来挺累人的。在我理解,replaceStaleEntry
只是做一个标记的作用,在各种情况下最后都会调用 cleanSomeSlots
来真正的清理过期条目。
你可以看到 ``
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); // 需要清理的 Entry
}
} while ( (n >>&