范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

hashMap源码解析

  问题
  此次主要分析1.8的版本, 并对1.8之前的结构做对比 hashMap结构 hashMap put的过程 hashMap 扩容的方式 hashMap的并发问题 1.8版本以及1.7版本的差异 hashMap的实现原理
  链表转成红黑树
  HashMap是一张哈希表(即数组),表中的每个元素都是键值对(Map.Entry类)。 当hash碰撞时。使用单向链表存储,新元素放在队尾 当单个链表大于8个时,转为红黑树进行存储,当链表长度小于6时,红黑树会重新变回链表 当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率 HashMap数据结构
  node结构 // Node是单向链表,它实现了Map.Entry接口     static class Node implements Map.Entry {         final int hash;         final K key;         V value;         Node next;          Node(int hash, K key, V value, Node next) {             this.hash = hash;             this.key = key;             this.value = value;             this.next = next;         }         // 略     }
  红黑树结构 static final class TreeNode extends LinkedHashMap.Entry {         TreeNode parent;  // red-black tree links         TreeNode left;         TreeNode right;         TreeNode prev;    // needed to unlink next upon deletion         boolean red;         TreeNode(int hash, K key, V val, Node next) {             super(hash, key, val, next);         }         // 略 什么是红黑树
  是一种平衡二叉树 节点是红色或者黑色 根节点是黑色 每个叶子的节点都是黑色的空节点 每个红色节点的两个子节点都是黑色的 从任意节点到其每个叶子的所有路径都包含相同的黑色节点
  通过左旋转或右旋转满足以上规则,保证深度相同 读写过程写入过程public V put(K key, V value) {           return putVal(hash(key), key, value, false, true);       }        /**       * Implements Map.put and related methods       *       * @param hash hash for key       * @param key the key       * @param value the value to put       * @param onlyIfAbsent if true, don"t change existing value       * @param evict if false, the table is in creation mode.       * @return previous value, or null if none       */   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                      boolean evict) {           Node[] tab;        Node p;        int n, i;           if ((tab = table) == null || (n = tab.length) == 0)               n = (tab = resize()).length;       /*如果table的在(n-1)&hash的值是空,就新建一个节点插入在该位置*/           if ((p = tab[i = (n - 1) & hash]) == null)               tab[i] = newNode(hash, key, value, null);       /*表示有冲突,开始处理冲突*/           else {               Node e;            K k;       /*检查第一个Node,p是不是要找的值*/               if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))                   e = p;               else if (p instanceof TreeNode)                   e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);               else {                   for (int binCount = 0; ; ++binCount) {           /*指针为空就挂在后面*/                       if ((e = p.next) == null) {                           p.next = newNode(hash, key, value, null);                  //如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,                            //treeifyBin首先判断当前hashMap的长度,如果不足64,只进行                           //resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树                           if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                               treeifyBin(tab, hash);                           break;                       }           /*如果有相同的key值就结束遍历*/                       if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))                           break;                       p = e;                   }               }       /*就是链表上有相同的key值*/               if (e != null) { // existing mapping for key,就是key的Value存在                   V oldValue = e.value;                   if (!onlyIfAbsent || oldValue == null)                       e.value = value;                   afterNodeAccess(e);                   return oldValue;//返回存在的Value值               }           }           ++modCount;        /*如果当前大小大于门限,门限原本是初始容量*0.75*/           if (++size > threshold)               resize();//扩容两倍           afterNodeInsertion(evict);           return null;       } HashMap的键值存储
  我们给 put() 方法传递键和值时,我们先对键调用 hashCode() 方法,计算并返回 hashCode,然后使用HashMap内部的hash算法,将hashCode计算为表中的具体位置,找到 Map 数组的 bucket 位置来储存 Node 对象。 解决Hash碰撞
  如果hash到的数组位置已存在对象,即为Hash碰撞。JDK使用拉链法解决Hash碰撞问题。
  即以原有的Node节点为基础,构造链表。将新的Node节点挂在后面。 链表过长导致的复杂度问题
  HashMap的查询操作最佳时间复杂度是O(1),但是当表中的某个链表过长时,查询该链表上的元素时间复杂度为O(n)。JDK1.8中解决了该问题,当HashMap中某链表长度大于8时,链表会重构为红黑树,这样,HashMap的最坏时间复杂度为O(n)。同理,为了不必要的消耗,当链表长度小于6时,红黑树会重新变回链表 读取过程public V get(Object key) {           Node e;           return (e = getNode(hash(key), key)) == null ? null : e.value;       }         /**       * Implements Map.get and related methods       *       * @param hash hash for key       * @param key the key       * @return the node, or null if none       */       final Node getNode(int hash, Object key) {           Node[] tab;//Entry对象数组       Node first,e; //在tab数组中经过散列的第一个位置       int n;       K k;       /*找到插入的第一个Node,方法是hash值和n-1相与,tab[(n - 1) & hash]*/       //也就是说在一条链上的hash值相同的           if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {       /*检查第一个Node是不是要找的Node*/               if (first.hash == hash && // always check first node                   ((k = first.key) == key || (key != null && key.equals(k))))//判断条件是hash值要相同,key值要相同                   return first;         /*检查first后面的node*/               if ((e = first.next) != null) {                   if (first instanceof TreeNode)                       return ((TreeNode)first).getTreeNode(hash, key);                   /*遍历后面的链表,找到key值和hash值都相同的Node*/                   do {                       if (e.hash == hash &&                           ((k = e.key) == key || (key != null && key.equals(k))))                           return e;                   } while ((e = e.next) != null);               }           }           return null;       }
  get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可 HashMap的扩容 final Node[] resize() {          Node[] oldTab = table;          int oldCap = (oldTab == null) ? 0 : oldTab.length;          int oldThr = threshold;          int newCap, newThr = 0;          /*如果旧表的长度不是空*/          if (oldCap > 0) {              if (oldCap >= MAXIMUM_CAPACITY) {                  threshold = Integer.MAX_VALUE;                  return oldTab;              }   /*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/              else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                       oldCap >= DEFAULT_INITIAL_CAPACITY)         /*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/                  newThr = oldThr << 1; // double threshold          }       /*如果旧表的长度的是0,就是说第一次初始化表*/          else if (oldThr > 0) // initial capacity was placed in threshold              newCap = oldThr;          else {               // zero initial threshold signifies using defaults              newCap = DEFAULT_INITIAL_CAPACITY;              newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);          }                               if (newThr == 0) {              float ft = (float)newCap * loadFactor;//新表长度乘以加载因子              newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                        (int)ft : Integer.MAX_VALUE);          }          threshold = newThr;          @SuppressWarnings({"rawtypes","unchecked"})   /*下面开始构造新表,初始化表中的数据*/          Node[] newTab = (Node[])new Node[newCap];          table = newTab;//把新表赋值给table          if (oldTab != null) {//原表不是空要把原表中数据移动到新表中                  /*遍历原来的旧表*/                    for (int j = 0; j < oldCap; ++j) {                  Node e;                  if ((e = oldTab[j]) != null) {                      oldTab[j] = null;                      if (e.next == null)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置                          newTab[e.hash & (newCap - 1)] = e;                      else if (e instanceof TreeNode)                          ((TreeNode)e).split(this, newTab, j, oldCap);   /*如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重*/                      else { // preserve order保证顺序                   ////新计算在新表的位置,并进行搬运                          Node loHead = null, loTail = null;                          Node hiHead = null, hiTail = null;                          Node next;                                                 do {                              next = e.next;//记录下一个结点             //新表是旧表的两倍容量,实例上就把单链表拆分为两队,                //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对                              if ((e.hash & oldCap) == 0) {                                  if (loTail == null)                                      loHead = e;                                  else                                      loTail.next = e;                                  loTail = e;                              }                              else {                                  if (hiTail == null)                                      hiHead = e;                                  else                                      hiTail.next = e;                                  hiTail = e;                              }                          } while ((e = next) != null);                                                 if (loTail != null) {//lo队不为null,放在新表原位置                              loTail.next = null;                              newTab[j] = loHead;                          }                          if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置                              hiTail.next = null;                              newTab[j + oldCap] = hiHead;                          }                      }                  }              }          }          return newTab;      } 扩容时机
  当size超过阈值(数组长度负载因子*)时,即开始扩容,HashMap的负载因子为0.75。 为何要数组未满就扩容
  避免频繁出现Hash碰撞,造成拉链过长(红黑树过长)。这样会导致查询复杂度频繁出现最坏情况 扩容过程
  创建原本数组容量*2的新数组,将节点从原本的数组中迁移过去。 jdk1.7的问题put()时,HashMap会先遍历table数组,用hash值和equals()判断数组中是否存在完全相同的key对象, 如果这个key对象在table数组中已经存在,就用新的value代替老的value。如果不存在,就创建一个新的Entry对象添加到table[ i ]处。如果该table[ i ]已经存在其他元素,那么新Entry对象将会储存在bucket链表的表头,通过next指向原有的Entry对象,形成链表结构(hash碰撞解决方案) 扩容时,将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置。
  问题点:两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了 为什么线程不安全
  HashMap 在并发时可能出现的问题主要是两方面:
  如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
  如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失

华为畅享50Pro来了骁龙6805000W三摄,采用旗舰造型众所周知,今天(7月27日)晚上将会迎来华为全新HarmonyOS3。0系统的发布会,不过在这场发布会开始之前,华为再次公布了另一款新机的发布预告华为畅享50Pro将于7月29日1雅克马尔安德烈博物馆豪宅里的艺术圣地不可能找到比这里更完美的会场了,这里聚集了如此多时髦优雅的名流宾客无一例外地闪闪发光正是这里的一切让安德烈的舞会在巴黎轰动一时,为这个时代留下了华丽的倩影镀金的双舞厅在上千盏蜡烛的故宫养心殿为啥长年潮湿阴冷?工作人员撬开地板,才发现端倪在中国,有句话叫作普天之下,莫非王土,率土之滨,莫非王臣。大致意思便是,天子也就是皇帝,富有四海,整个天下都是皇帝一个人的。而皇帝定都的地方,便是国家的首都,皇帝居住的地方,便叫作独库公路这一幕惹众怒!可怕的事情还是发生了最可怕的事,终究还是发生了看到新闻的时候,完全不想去相信。时至今日,这样令人心痛不已的现象还是会发生在绿油油的草丛中惊险刺激的公路上美丽如画的风景里。请不要让独库公路和森林草原变成中国希腊直飞航班即将恢复据最新消息此前一直停航的北京雅典直飞航班将于2022年7月27日恢复,入境点为成都。以下是具体航班信息,一起来看看吧!Part。01航班信息航线北京雅典成都北京班期周三航班时刻CA我国的三大海峡,沟通了哪些海域?海峡指的是位于两个大陆之间,或者大陆和岛屿之间,岛屿和岛屿之间的狭窄水道。海峡起着沟通不同海域的重要作用,通常是海上交通咽喉,比如马六甲海峡,沟通了太平洋和印度洋,土耳其海峡,沟通夏天最适合吃的补脾瓜,补中益气还能抗癌,填满你丢失的气血大家好,我是米医生。现在暑湿季节,不论室内室外都是热气腾腾又潮湿,闷热难耐得很。很多来门诊看病的小伙伴都表示最近没精打采,容易头昏脑涨,胃口不好,甚至变得消瘦等等状况。为啥?因为暑建议中老年天热多吃6种含天然高叶酸蔬菜,精神饱满过炎夏建议中老年天热多吃6种含天然高叶酸蔬菜,精神饱满过炎夏,烈日炎炎的夏天很容易让人犯困,从而出现疲劳和健忘的情况,特别是上了年纪的中老人更是如此,夏天经常犯困的中老年朋友,平时可以多吃西瓜降血糖,升血糖?有一个部位可以放心吃,糖尿病患者看看要说夏天什么最显眼,那莫属于水果摊上一切两半那红彤彤非常扎眼的西瓜了。夏天是西瓜的季节,就算是热销水果价格也是非常的平价,所以西瓜也是大家心目中消暑水果第一名。但有一种人,往往在这口腔细菌诱发炎症伤血管!你是否错过了黄金刷牙时间?导语人与动脉同寿,因此要想健康长寿,首要任务是保证动脉血管的健康。口腔细菌产生的霉素可进入全身血液循环,损伤血管内皮。如此一来,好好刷牙很重要!相关调查研究发现,口腔细菌可诱发动脉夏天喝茶有讲究,行家牢记这3不喝,讲究多好处也多老话说夏有三伏,热在中伏,本来入伏以后天气就很炎热,那接下来岂不是热上加热!这样的高温天气可是让很多人无福消受啊,都想方设法的来缓解暑热。但是,不少人选择了错误的方法,非但没有解暑
经常睡到半夜34点醒,再难入睡是怎么回事?多数是3个原因引起林大妈已经68岁了,近些年越来越睡不好了,最近这段时间甚至还常常出现了凌晨34点就醒的情况。听人说,泡脚按摩等方式可以改善睡眠,便也去尝试并坚持了一段时间,但还是没什么效果,这让林酒店一到半夜就满房?这几个原因太现实了酒店为什么到了凌晨就会满房呢?听到前台小姐的这几个回答,很多网友表示太现实了。(此处已添加小程序,请到今日头条客户端查看)不知道大家外出旅行的时候有没有发现这种情况,如果是在半夜凌1961年陈赓病痛难眠,凝望妻子陈赓深情道傅涯,你怎么不看看我前言2003年,85岁的傅涯在回忆自己的丈夫陈赓时说道转瞬之间,他离我而去已经44年了,他的神态时常浮现在我的眼前,20多年相识相知,共同生活与战斗的不少往事,至今都难以忘怀。图陈冬天,腌酸菜就用这个方法!即便是罐头瓶,也能腌出好酸菜又到了分享美食的时间,我是高兴。在楼房里腌酸菜是不容易的!俗话说小雪腌菜,大雪腌肉。以前,北方人腌酸菜都是用缸(酸菜缸),现在家家户户住楼房,都不喜欢用酸菜缸。一是因为酸菜缸太大了新版暗影主宰巨帅,钟无艳双喜临门,官方再送荣耀积分王者荣耀新赛季已经在体验服上线,还是和往常一样,每一次新赛季到来之前都会在体验服大整改一次。这一次官方也再次调整了大量装备,其中包含了法师装备和坦克装备,而且还给法师英雄安排了一件詹姆斯阳转阴回归,掀翻快船!霍华德打服沃格尔,进入首发阵容12月1日,湖人前往萨克门托对阵国王,詹姆斯在赛前新冠检测中,第一次呈阳性,随后又检测了一次变为阴性了,之后又做了一次又是阳性,于是根据联盟防疫规定,詹姆斯被迫回洛杉矶进行隔离!回雨童连线业内人士iG续约上单预算不足,Uzi合同比较复杂昨日,解说雨童和Kirs在直播间连线了某个圈内大佬,与他聊到了一些关于转会期的消息。关于Uzi雨童小狗复出好像是确定的事情,至于在哪个队大家也都知道,我们待会再聊待会问神秘人。关于63岁李幼斌和众星聚会,餐桌遭大咖辱骂反赔笑脸,网友太憋屈明星个人的名气与他们在娱乐圈的地位密不可分,很多的明星名气较大,因此在娱乐圈的地位也会更高一些。就像很多的明星所说,为什么希望自己能够红起来,就是在自己有了更大的名气之后,出席任何太阳让了半个布克,勇士还是输球?2日NBA热火vs骑士快船vs国王这场比赛布克只打了15分钟,因左腿拉伤,后来再也没有出场。就在这种情况下,太阳队以10496击败勇士队,数据方面,勇士队的普尔28分5篮板3助攻2抢断,库里12分3篮板2助攻,维金NBA人生大赢家!30岁立陶宛巨兽手握3亿合同,娶极品白富美娇妻在最近一场鹈鹕对阵快船队的比赛中,鹈鹕队以123104的比分战胜了来势汹汹的快船队,这让很多球迷意外。快船是NBA中的强队,没想到这次被鹈鹕击落马下,鹈鹕这个赛季的状态看来是非常不00010!5年9000万,拿了大合同就划水,你就是NBA的大骗子热火近来连续遭遇伤病的困扰,上一场巴特勒和希罗不打,结果他们在主场惨败给了掘金,今天面对骑士的比赛,希罗复出而阿德巴约又因伤缺阵,这让球队的阵容和打法都是受到了很大的影响,而骑士则