jdk1.7hashMap扩容
当我们插入值时,如果hashMap数组达到阈值且当前数组位置不为空,那么它就会进行扩容。但在jdk1.7版本中,多线程下,多个线程对一个hashMap操作是会产生环链的。
下面是hashMap扩容将元素转移的主要代码,也是环链产生的地方。
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
假如有两个线程P1、P2,以及table[]某个节点链表为 a->b->null(a、b是HashMap的Entry节点,保存着Key-Value键值对的值)
P1先执行,执行完"Entry<K,V> next = e.next;"代码后,P1发生阻塞或者其他情况不再执行下去,此时e=a,next=b。(自己画的图,比较丑,见谅)
P1阻塞后P2获得CPU资源开始执行,由于P1并没有执行完transfer(),table 和 threshold仍为原来的值,P2依旧会进行resize操作,并且P2顺利执行完resize()方法,假设a、b节点仍然rehash到newTable[](注意,P1和P2中newTable[]不是同一个)中同一个节点链表中,则新的节点链表为 b->a->null。此时线程一的e和next仍然指着a 和 b
P1又继续执行"Entry<K,V> next = e.next;"之后的代码,则newTable[i]的节点链表变化过程为:
第一次while循环,newTable[i]=a,链表为:b->a->null;此时e=b;
进入第二次循环,newTable[i]=b,数据全部转移到p1表
第三次循环则产生了环链B.next=A; A.next=B
jdk1.8hashMap扩容优化
jdk1.8在扩容时引入了高低位指针的概念。
将节点的hash直接与数组长度进行与运算,结果只为0和不为0两种结果。
这里为了方便理解我们假设数组长度为16。之前通过hash & (n- 1)获得hash值的第四位从而确定数组位置,但第五位我们并不知道,所以通过e.hash & oldCap计算出当前位置链表中所有节点的第五位是否为1。将所有第五位为0的节点放入lo链表,反之则放入hi链表。
000 0 0101——————》0留在原地 lo链表
000 1 0101——————》1移向高位 hi链表
001 0 0101——————》0留在原地 lo链表
001 1 0101——————》1移向高位 hi链表
在移动链表时,lo链表仍然放入新数组的当前位置,而hi链表则放入(久数组位置+久数组数组长度)的位置,这恰好对应扩容后位置计算表达式hash & (32- 1),前提是数组长度为2的整数次幂。
这样设计十分巧妙,直接带来了三个好处。
1、不需要重新再计算hash,性能得到提升;
2、放过去的链表内元素的相对顺序不会改变;
3、不会在并发扩容中产生环链的情况。