获取容器元素的方法:
// 根据key快速查找entry节点
private Entry getEntry(ThreadLocal<?> key) {
// 通过threadLocal对象(key)的hash值计算数组下标
int i = key.threadLocalHashCode & (table.length - 1);
// 取对应下标元素
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 查找不到有两种情况:
// 1.对应下标桶位为空
// 2对应下标桶位元素不是key关联的entry(开放地址解决hash冲突导致的)
return getEntryAfterMiss(key, i, e);
}
// 初次查找失败后再次查找entry节点
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
// 获取entry数组及长度
Entry[] tab = table;
int len = tab.length;
// 如果e为null,说明对应下标桶位为空,找不到key对应的entry
// 如果e不为null,则用解决hash冲突时的方法(顺延数组下一位)一直找下去,直到找到或e为null
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
// 在寻找的过程中如果节点的key,即ThreadLocal已经被回收(被弱引用的对象可能会被回收)
// 则移除过时的节点,移除过时节点的方法分析见移除元素部分
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
// 没有找到,返回null
return null;
}
ThreadLocalMap-增加和修改元素
增加和修改容器元素的方法:
这里在根据hash值计算出下标后,由于是开放地址解决hash冲突,会顺序向后遍历直到遇到null或遇到key对应的节点。
这里会出现三种情况:
case1:遍历时找到了key对应节点,这时直接修改节点的值即可;
case2:遍历中遇到了有过时的节点(key被回收的节点);
case3:遍历没有遇到过时的节点,也没有找到key对应节点,说明此时应该插入新节点(用输入键值构造新节点)。因为是增加新元素,所以可以容量会超过阈值。在删除节点后容量如果超过阈值,则要进行扩容操作。
private void set(ThreadLocal<?> key, Object value) {
// 获取数组,计算key对应的数组下标
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 从下标i开始,顺序遍历数组(顺着hash冲突开放地址的路径),直到节点为null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 获取遍历到的节点的key
ThreadLocal<?> k = e.get();
// case1:命中key,说明已存在key对应节点,修改value值即可
if (k == key) {
e.value = value;
return;
}
// case2:如果遍历到的节点的key为null,说明该threadLocal对象已经被回收
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// case3:遍历节点直到null都没有找到对应key,说明map中没有key对应entry
// 则在该位置用输入键和值新建一个entry节点
tab[i] = new Entry(key, value);
int sz = ++size;
// 判断是否清理过时节点后,在判断是否需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
case2:增加和修改过程中遇到已经过时的节点的处理。这里的参数staleSlot表示key计算的下标开始往后遇到的第一个过时节点,不管map中有无key对应的节点,该位置之后一定会存入key的节点。这里定义了一个变量slotToExpunge,其含义是左右连续非null的entry块中第一个过时节点(记录该位置是为了后续清除过时节点可以从slotToExpunge处开始)。示意如下:
这步操作有两种情况:
casse2.1:从过时节点staleSlot往后查找遇到key对应节点,则将staleSlot处节点与key对应节点交换。然后清除整理连续块。
casse2.2:没遇到key对应节点,说明map中不存在key对应节点,则新建一个节点填入staleSlot处。然后清除整理连续块。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
// 获取entry数组和长度
Entry[] tab = table;
int len = tab.length;
Entry e;
// 往前移动寻找第一个过时节点(直到遇到null),如果没找到的话说明第一个过时节点为staleslot处节点
// slotToExpunge表示连续块中第一个过时节点
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 从输入下标staleSlot向后找到第一个出现的key对应的节点或过时的节点(key被回收的节点)
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// case2.1:如果找到key对应的节点,则用staleSlot处节点和该节点交换,以保持hash表的顺序(hash冲突时顺序向后寻找)
// 交换后的staleSlot节点及其之前的过时节点会被清除
if (k == key) {
// 交换staleSlot处节点和key对应节点
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 更新slotToExpunge的值,使其保持连续块中第一个过时节点的特性,方便后续清理过时节点。
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 从slotToExpunge开始清除整理连续块
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果遇到过时节点,更新slotToExpunge的值
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// case2.2:没有找到key对应节点,增加新节点并填入staleSlot处
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 这里如果slotToExpunge=staleSlot,说明连续块中只有一个过时节点,且已经被新建节点填入,就不需要再整理。
// 如果除了原staleSlot处,还有其它过时节点,从slotToExpunge开始清除整理连续块
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}