京东面试题之:HashMap 链表循环问题
3月19日晚上参加了京东base成都的研发工程师电话面试,其中对面试官提出的一个问题印象比较深,特别记录一下;
Q1: 你能聊聊hashMap扩容机制嘛?
A1:是这样的,在JDK1.7及以前,hashmap在判断是否需要扩容前,需要满足两个条件
①会先去比较当前的enrty数量是否达到阈值(初始长度*负载因子),如果达到了则进入②
②再去判断当前的key所计算出来的hash值是否会产生hash冲突?如果会 则进行扩容,扩容为原来的两倍 然后在把原来的enrty节点放到新的map中,在此之前需要rehash
Q2: 那么在扩容的时候会出现什么问题呢?
A2:可能会造成链表循环把........
Q3:那你能聊聊为什么会造成链表循环呢? 是如何解决的呢?
A3:.........10:40:51
面试完后马上看了源码,然后进行了分析如下:
jdk 1.7 hashmap
1.7中是数据是先扩容后插入
链表循环问题发生在链表转移的方法中
1 void transfer(Entry[] newTable, boolean rehash) { 2 int newCapacity = newTable.length; 3 for (Entry<K,V> e : table) { 4 while(null != e) { 5 Entry<K,V> next = e.next; 6 if (rehash) { 7 e.hash = null == e.key ? 0 : hash(e.key); 8 } 9 int i = indexFor(e.hash, newCapacity); 10 e.next = newTable[i]; 11 newTable[i] = e; 12 e = next; 13 } 14 } 15 }
如果元素个数已经达到数组阈值,则扩容,并把原来的元素移动过去。
假设HashMap初始化大小为4,插入个3节点,不巧的是,这3个节点都hash到同一个位置,如果按照默认的负载因子的话,插入第3个节点就会扩容,为了验证效果,假设负载因子是1
插入第4个节点时,发生rehash,假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组。
假设 线程2 在执行到Entry<K,V> next = e.next;
之后,cpu时间片用完了,这时变量e指向节点a,变量next指向节点b。
线程1继续执行,很不巧,a、b、c节点rehash之后在同一个位置7,开始移动节点
第一步,移动节点a
第二步,移动节点b
注意,这里的顺序是反过来的,继续移动节点c
这个时候 线程1 的时间片用完,内部的table还没有设置成新的newTable, 线程2 开始执行,这时内部的引用关系如下:
这时,在 线程2 中,变量e指向节点a,变量next指向节点b,开始执行循环体的剩余逻辑
1 Entry<K,V> next = e.next; 2 int i = indexFor(e.hash, newCapacity); 3 e.next = newTable[i]; 4 newTable[i] = e; 5 e = next;
执行之后的引用关系如下图
执行后,变量e指向节点b,因为e不是null,则继续执行循环体,执行后的引用关系
变量e又重新指回节点a,只能继续执行循环体,这里仔细分析下:
1、执行完Entry<K,V> next = e.next;
,目前节点a没有next,所以变量next指向null;
2、e.next = newTable[i];
其中 newTable[i] 指向节点b,那就是把a的next指向了节点b,这样a和b就相互引用了,形成了一个环;
3、newTable[i] = e
把节点a放到了数组i位置;
4、e = next;
把变量e赋值为null,因为第一步中变量next就是指向null;
所以最终的引用关系是这样的:
jdk 1.8 hashmap
1.8中数据是先插入再扩容
1 //规避了8版本以下的成环问题 2 else { // preserve order 3 // loHead 表示老值,老值的意思是扩容后,该链表中计算出索引位置不变的元素 4 // hiHead 表示新值,新值的意思是扩容后,计算出索引位置发生变化的元素 5 // 举个例子,数组大小是 8 ,在数组索引位置是 1 的地方挂着一个链表,链表有两个值,两个值的 hashcode 分别是是9和33。 6 // 当数组发生扩容时,新数组的大小是 16,此时 hashcode 是 33 的值计算出来的数组索引位置仍然是 1,我们称为老值 7 // hashcode 是 9 的值计算出来的数组索引位置是 9,就发生了变化,我们称为新值。 8 Node<K,V> loHead = null, loTail = null; 9 Node<K,V> hiHead = null, hiTail = null; 10 Node<K,V> next; 11 // java 7 是在 while 循环里面,单个计算好数组索引位置后,单个的插入数组中,在多线程情况下,会有成环问题 12 // java 8 是等链表整个 while 循环结束后,才给数组赋值,所以多线程情况下,也不会成环,也就是找到链表最后一个值之后才赋值,尾插法 13 do { 14 next = e.next; 15 // (e.hash & oldCap) == 0 表示老值链表 16 if ((e.hash & oldCap) == 0) { 17 if (loTail == null) 18 loHead = e; 19 else 20 loTail.next = e; 21 loTail = e; 22 } 23 // (e.hash & oldCap) == 0 表示新值链表 24 else { 25 if (hiTail == null) 26 hiHead = e; 27 else 28 hiTail.next = e; 29 hiTail = e; 30 } 31 } while ((e = next) != null); 32 // 老值链表赋值给原来的数组索引位置 33 if (loTail != null) { 34 loTail.next = null; 35 newTab[j] = loHead; 36 } 37 // 新值链表赋值到新的数组索引位置 38 if (hiTail != null) { 39 hiTail.next = null; 40 newTab[j + oldCap] = hiHead; 41 } 42 }