JDK1.7的rehash算法: 对所有元素进行重新hash寻址然后再放入到新的位置
// transfer函数完成旧表到新表的迁移
// transfer这个方法,在多线程环境下会乱套,死循环问题就是发生在这个函数里
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//遍历同桶数组中的每一个桶
while(null != e) {//顺序遍历某个桶的外挂链表
Entry<K,V> next = e.next;//引用next
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
e.next = newTable[i];//头插法插入新表中
newTable[i] = e;
e = next;
}
}
}
hash寻址的一个特点 :位置要么是在原来的index,要么实在原来的index + oldCap的位置
计算下标的算法是index = (n - 1) & hash,其中的 n - 1的二进制是1111....
当数组扩容之后n变成了原来的二倍--》n - 1的二进制前面多了一个1,而hash值变,那么计算出的下标就有两种情况
oldIndex = newIndex(新增1的那位对应的hash那位是0 || oldIndex + 原来数组的长度 = newIndex(111->1111:原来是7,扩成了15)
如何判断是在oldIndex 还是 oldIndex + oldCap? 答案:通过hash & n(n的二进制只有一位是1,也是我们想判断的那一位)
其实rehash就是遍历数组的每个位置,判断节点的状态,是单个或者链表或者红黑树。接下来就每种情况进行讨论
- 单个节点: 其实重新进行hash寻址算法,找到对应数组的下标,放上就行了(此处没有优化)
- 链表:将之前的链表rehash之后拆分为了两个链表(一个while循环遍历链表,判断每一个节点如果(hash & n) == 1,把这个节点挂在到链表A,反之挂到B,遍历完成后,把两个链表整个挪过去)一个链表在当前的位置index,另一个位置变成了index + oldCap
- 红黑树:其实原理跟链表的差不多,红黑树这个拆成两个红黑树,分别挂到新的数组的位置上,最后还要判断两个红黑树是需要退化成链表还是继续是红黑树
结论:JDK1.8的rehash算法优化就是对原来的链表或者红黑树进行拆分成两部分,然后分别挂在oldIndex 和 oldIndex + oldCap的位置,这样做就避免了大量的节点进行重新hash寻址算法和重新放到hash表的过程,大大增加了扩容效率