ThreadLocal与ThreadLocalMap源码分析 (4)

获取容器元素的方法:

// 根据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处开始)。示意如下:

ThreadLocal与ThreadLocalMap源码分析

这步操作有两种情况:

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

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzpjdg.html