2021年3月31日星期三

HashMap底层原理分析

本文将从以下方面结合源码进行分析:自动扩容、初始化与懒加载、哈希计算、位运算与并发,(默认采用JDK1.8)。
 
自动扩容
扩容操作发生在putVal最后部分,在增加元素后才判断是否需要扩容,如果超过阈值,会自动扩容。

 
 
 
 
 
 
 
 
 
 
这里扩容都是<<1翻倍进行扩容的。

扩容时节点数组进行数据转移的三种情况:
  • 节点的元素无后继节点:

  直接根据节点hash值重新计算下标,然后复制到新的数组中。

  • 节点为树节点:

  进行红黑树的扩容操作。

  因为capacity变化后,hash&(cap-1)可能得到不同结果。原有的红黑树变成高低位两个红黑树。低位红黑树下标位置和旧数组相同,高位红黑树下标位置在旧数组的基础上+oldCap,因为hash&(2*cap-1)结果等于hash&(cap-1)或者hash&(cap-1)+cap。

   红黑树扩容时遍历原有链表,然后根据新的hash值重新分为低位链表和高位链表。
  若所有元素都在低位链表或高位链表,则不需要重新树化,直接将链表头节点插入数组对应位置;
  若低位链表或高位链表的数量<7,则深拷贝低位或高位树节点链表得到普通节点新链表(低位或高位树节点链表含有树的偏序关系,拷贝得到的普通节点链表只有链表的偏序关系),并将新链表头节点插入数组对应位置。
  具体源码分析如下:


 1 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { 2  // 获取自身树节点 3  TreeNode<K,V> b = this; 4  // Relink into lo and hi lists, preserving order 5  // 低位链表的头尾节点 6  TreeNode<K,V> loHead = null, loTail = null; 7  // 高位链表的头尾节点 8  TreeNode<K,V> hiHead = null, hiTail = null; 9  // 低位链表节点数量、高位链表节点数量10  int lc = 0, hc = 0;11  for (TreeNode<K,V> e = b, next; e != null; e = next) {12   next = (TreeNode<K,V>)e.next;13   // 这步操作不是多余的,在e为低位或高位链表最终尾节点时起到赋空作用14   e.next = null;15   // 如果仍然在原位置,则加入低位链表16   if ((e.hash & bit) == 0) {17    if ((e.prev = loTail) == null)18     loHead = e;19    else20     loTail.next = e;21    loTail = e;22    ++lc;//低位链表数量+123   }24   else {25    // 如果是在新的位置(原索引值+oldcap),加入高位链表26    if ((e.prev = hiTail) == null)27     hiHead = e;28    else29     hiTail.next = e;30    hiTail = e;31    ++hc;// 高位链表数量+132   }33  }34  // 低位链表不为空35  if (loHead != null) {36   // 低位链表数量不超过6,则深拷贝低位树节点链表得到普通节点新链表,并将新链表头部放入数组37   if (lc <= UNTREEIFY_THRESHOLD)38    tab[index] = loHead.untreeify(map);39   else {40    tab[index] = loHead;41    // 如果高位链表为空,说明全部元素都在低位链表中,因为原链表已经是树化的了,所以不用再转为红黑树42    if (hiHead != null) // (else is already treeified)43     loHead.treeify(tab);44   }45  }46  // 高位链表不为空47  if (hiHead != null) {48   // 高位链表数量不超过6,则深拷贝树节点高位链表得到普通节点新链表,并将新链表头部放入数组49   if (hc <= UNTREEIFY_THRESHOLD)50    tab[index + bit] = hiHead.untreeify(map);51   else {52    tab[index + bit] = hiHead;53    // 如果低位链表为空,说明全部元素都在高位链表中,因为原链表已经是树化的了,所以不用再转为红黑树54    if (loHead != null)55     hiHead.treeify(tab);56   }57  }58 }

 

  • 节点为链表节点:

  进行链表的复制操作。操作和红黑树扩容操作非常相似。也是先遍历原有链表节点,然后根据新的hash值分为低位链表和高位链表。
  分完高低位链表后,将头节点插入数组对应位置即可。
  具体源码分析如下:
 1 // case3:节点为链表节点,进行链表的赋值操作  2 else { // preserve order 3  // 低位Node链表头节点和尾节点 4  Node<K,V> loHead = null, loTail = null; 5  // 高位Node链表头节点和尾节点 6  Node<K,V> hiHead = null, hiTail = null; 7  Node<K,V> next; 8  // 遍历原链表,拆分成低位链表和高位链表 9  do {10   next = e.next;11   // 如果是在原位置,则加入低位链表12   if ((e.hash & oldCap) == 0) {13    if (loTail == null)14     loHead = e;15    else16     loTail.next = e;17    loTail = e;18   }19   else {20    // 如果不在原位置,加入高位链表21    if (hiTail == null)22     hiHead = e;23    else24     hiTail.next = e;25    hiTail = e;26   }27  } while ((e = next) != null);28  // 如果低位链表不为空29  if (loTail != null) {30   // 尾部节点赋空并将头部节点放入数组指定位置31   loTail.next = null;32   newTab[j] = loHead;33  }34  // 如果高位链表不为空35  if (hiTail != null) {36   // 尾部节点赋空并将头部节点放入数组指定位置37   hiTail.next = null;38   newTab[j + oldCap] = hiHead;39  }40 }

 在jdk1.8之前,hashmap在多线程环境中使用会出现死链问题。如果有多个线程同时进行扩容操作,一个线程拿到链表头节点和后继节点时挂起,另一个线程执行完扩容操作,会使得这两个节点互相依赖,出现死链,导致第一个线程不能退出循环,CPU使用率飙升。

jdk1.8将原来的头插法改为了尾插法,同时复制链表时不再是遍历一个节点就插入,而是使用高低位链表。待遍历完所有节点后,再将高低位链表放入新数组对应位置。

但是仍然不建议在多线程环境下使用,仍然会有数据缺失和数据重复等等问题。



 















原文转载:http://www.shaoqun.com/a/654745.html

跨境电商:https://www.ikjzd.com/

心怡:https://www.ikjzd.com/w/1327

笨鸟海淘:https://www.ikjzd.com/w/1550


本文将从以下方面结合源码进行分析:自动扩容、初始化与懒加载、哈希计算、位运算与并发,(默认采用JDK1.8)。自动扩容扩容操作发生在putVal最后部分,在增加元素后才判断是否需要扩容,如果超过阈值,会自动扩容。这里扩容都是<<1翻倍进行扩容的。扩容时节点数组进行数据转移的三种情况:节点的元素无后继节点:  直接根据节点hash值重新计算下标,然后复制到新的数组中。节点为树节点:  进
mav:https://www.ikjzd.com/w/2414
c2c:https://www.ikjzd.com/w/1576
6pm:https://www.ikjzd.com/w/317
"杀虫剂"错杀风又来?卖家赶紧防备起来!:https://www.ikjzd.com/home/105201
两女双飞,,,用力_女人口述二女伺候一男做:http://www.30bags.com/m/a/254325.html
我和学妹在图书馆里做:口述图书馆里初夜激情后:http://lady.shaoqun.com/m/a/273518.html

没有评论:

发表评论