zoukankan      html  css  js  c++  java
  • HashMap原理

    结构

    数组+链表 结构 ;

    static class Node<K,V> implements Map.Entry<K,V> {

        final int hash;

        final K key;

        V value;

        Node<K,V> next; //同buket上下一个节点

    }

    transient Node<K,V>[] table; //初始化是个null,第一次put时候才有值

    hash算法

    static final int hash(Object key) {

      int h;

      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //>>>无符号右移

    }

    key的hash值, 高16位 和低16位异或操作, 这样比较均匀分散位置 ;

    put操作图

    get 操作图

    如果节点是红黑树, 则根据hash值 大小,分别从root节点,左右边查找, 红黑树特性 左边hash值 <右边, 一直往下找即可;

    resize扩容

    final Node<K,V>[] resize() {

      Node<K,V>[] 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; // 超过最大值,不在扩容

        }

        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

          oldCap >= DEFAULT_INITIAL_CAPACITY)

            newThr = oldThr << 1; // 否则扩大为原来的 2 倍

       }

      else if (oldThr > 0) /// 初始化时,threshold 暂时保存 initialCapacity 参数的值

        newCap = oldThr;

      else { // zero initial threshold signifies using defaults

        newCap = DEFAULT_INITIAL_CAPACITY;

        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

      }

      if (newThr == 0) { // 计算新的 resize 上限

        float ft = (float)newCap * loadFactor;

        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

          (int)ft : Integer.MAX_VALUE);

      }

      threshold = newThr;

      @SuppressWarnings({"rawtypes","unchecked"})

      Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

      table = newTab;

      if (oldTab != null) { // 将旧的键值对移动到新的哈希桶数组中

        for (int j = 0; j < oldCap; ++j) {

          Node<K,V> e;

          if ((e = oldTab[j]) != null) {

            oldTab[j] = null;

            if (e.next == null) // 无链条

              newTab[e.hash & (newCap - 1)] = e;

            else if (e instanceof TreeNode) // 拆红黑树,先拆成两个子链表,再分别按需转成红黑树

              ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

            else { // 拆链表,拆成两个子链表并保持原有顺序

              Node<K,V> loHead = null, loTail = null;

              Node<K,V> hiHead = null, hiTail = null;

              Node<K,V> next;

              do {

                next = e.next;

                if ((e.hash & oldCap) == 0) { // 原位置不变的子链表

                  if (loTail == null)

                    loHead = e;

                  else

                    loTail.next = e;

                  loTail = e;

                }

                else { // 原位置偏移 oldCap 的子链表( 原索引+oldCap)

                  if (hiTail == null)

                    hiHead = e;

                  else

                    hiTail.next = e;

                  hiTail = e;

                }

              } while ((e = next) != null);

              // 放到新的哈希桶中(// 原索引放到bucket里)

                if (loTail != null) {

                  loTail.next = null;

                  newTab[j] = loHead;

                }

                if (hiTail != null) { // 原索引+oldCap放到bucket里

                  hiTail.next = null;

                  newTab[j + oldCap] = hiHead;

                }

              }

            }

          }

        }

        return newTab;

    }

    在重新计算链表中元素位置时只可能得到两个子链表(高链表和低链表):索引不变的元素链表和有相同偏移量的元素链表。在构造子链表的过程中,使用头节点和尾节点,保证了拆分后的有序性:

    查看 TreeNode.split() 方法发现,红黑树拆分的逻辑和链表一样,只不过在拆分完成后,会根据子链表的长度做以下处理:

    • 长度小于 6,返回一个不包含 TreeNode 的普通链表
    • 否则,把子链表转为红黑树

    红黑树之所以能够按照链表的逻辑拆分,是因为链表在转红黑树时,保留了原链表的链条引用,(node.next )这样也方便了遍历操作。

    final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {

      TreeNode<K,V> b = this;

      // Relink into lo and hi lists, preserving order

      TreeNode<K,V> loHead = null, loTail = null;

      TreeNode<K,V> hiHead = null, hiTail = null;

      int lc = 0, hc = 0;

      for (TreeNode<K,V> e = b, next; e != null; e = next) {

        next = (TreeNode<K,V>)e.next;

        e.next = null;

          if ((e.hash & bit) == 0) {

          if ((e.prev = loTail) == null)

            loHead = e;

          else

            loTail.next = e;

          loTail = e;

          ++lc;

        }

        else {

          if ((e.prev = hiTail) == null)

            hiHead = e;

         else

            hiTail.next = e;

          hiTail = e;

          ++hc;

        }

      }

      if (loHead != null) {

        if (lc <= UNTREEIFY_THRESHOLD) //长度小于=6

          tab[index] = loHead.untreeify(map); //转换成链表

        else {

          tab[index] = loHead;

          if (hiHead != null) // (else is already treeified)

            loHead.treeify(tab); //转成红黑树

          }

      }

      if (hiHead != null) {

        if (hc <= UNTREEIFY_THRESHOLD) //长度小于=6 转换成链表

          tab[index + bit] = hiHead.untreeify(map);

        else {

          tab[index + bit] = hiHead;

          if (loHead != null)

            hiHead.treeify(tab);

        }

      }

    }

    其他参考图:

    链表转红黑树

    链表转红黑树主要做了以下几件事:

    1. 判断桶容量是否达到树化的最低要求,否则进行扩容
    2. 将原链表转为由 TreeNode 组成的双向链表
    3. 将新链表转为红黑树

    代码如下:

    final void treeifyBin(Node<K,V>[] tab, int hash) {

      int n, index; Node<K,V> e;

      if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)

        resize(); // 如果哈希桶容量小于树化的最小容量64,优先进行扩容

      else if ((e = tab[index = (n - 1) & hash]) != null) {  // 通过hash求出bucket的位置

        TreeNode<K,V> hd = null, tl = null;

        do {

          TreeNode<K,V> p = replacementTreeNode(e, null); // 将普通节点转为树形节点TreeNode

          if (tl == null)

            hd = p;

          else {

            p.prev = tl; // 把原来的单链表转成了双向链表

            tl.next = p;

          }

          tl = p; 

        } while ((e = e.next) != null);

        if ((tab[index] = hd) != null)

          hd.treeify(tab); // 将TreeNode链表转为红黑树

      }

    }

    TreeNode既是树形结构 又是链表结构 ;

    treeify方法:

    final void treeify(Node<K,V>[] tab) {

      TreeNode<K,V> root = null;

      // 以for循环的方式遍历刚才我们创建的链表。

      for (TreeNode<K,V> x = this, next; x != null; x = next) {

        next = (TreeNode<K,V>)x.next; // next向前推进。

        x.left = x.right = null;

        if (root == null) { // 为树根节点赋值。

          x.parent = null;

          x.red = false;

          root = x;

        }

        else { // x即为当前访问链表中的项。

          K k = x.key;

          int h = x.hash;

          Class<?> kc = null;

          // 此时红黑树已经有了根节点,上面获取了当前加入红黑树的项的key和hash值进入核心循环。

          // 这里从root开始,是以一个自顶向下的方式遍历添加。

          // for循环没有控制条件,由代码内break跳出循环。

          for (TreeNode<K,V> p = root;;) {

            // dir:directory,比较添加项与当前树中访问节点的hash值判断加入项的路径,-1为左子树,+1为右子树。

            // ph:parent hash。

            int dir, ph;

            K pk = p.key;

            if ((ph = p.hash) > h)

              dir = -1;

            else if (ph < h)

              dir = 1;

            else if ((kc == null &&

              (kc = comparableClassFor(k)) == null) ||

              (dir = compareComparables(kc, k, pk)) == 0)

              dir = tieBreakOrder(k, pk);

            TreeNode<K,V> xp = p; // xp:x parent。

                   // 找到符合x添加条件的节点。

            if ((p = (dir <= 0) ? p.left : p.right) == null) {

              x.parent = xp;

              if (dir <= 0) // 如果xp的hash值大于x的hash值,将x添加在xp的左边。

                xp.left = x;

              else // 反之添加在xp的右边。

                xp.right = x;

              root = balanceInsertion(root, x); // 维护添加后红黑树的红黑结构。

              break; // 跳出循环当前链表中的项成功的添加到了红黑树中。

            }

          }

        }

      }

      moveRootToFront(tab, root);

    }

    balanceInsertion

    第一次循环会将链表中的首节点作为红黑树的根,而后的循环会将链表中的的项通过比较hash值然后连接到相应树节点的左边或者右边,插入可能会破坏树的结构所以接着执行balanceInsertion

    static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,

    TreeNode<K,V> x) {

      x.red = true; // 正如开头所说,新加入树节点默认都是红色的,不会破坏树的结构。

      // 这些变量名不是作者随便定义的都是有意义的。

      // xp:x parent,代表x的父节点。

      // xpp:x parent parent,代表x的祖父节点

      // xppl:x parent parent left,代表x的祖父的左节点。

      // xppr:x parent parent right,代表x的祖父的右节点。

      for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {

        // 如果x的父节点为null说明只有一个节点,该节点为根节点,根节点为黑色,red = false。

        if ((xp = x.parent) == null) {

          x.red = false;

          return x;

        }

        // 进入else说明不是根节点。

        // 如果父节点是黑色,那么大吉大利,红色的x节点可以直接添加到黑色节点后面,返回根就行了不需要任何多余的操作。

        // 如果父节点是红色的,但祖父节点为空的话也可以直接返回根此时父节点就是根节点,因为根必须是黑色的,添加在后面没有任何问题。

        else if (!xp.red || (xpp = xp.parent) == null)

          return root;

        // 一旦我们进入到这里就说明了两件是情

        // 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,所以必须经过旋转变换。

        // 2.x的祖父节点xpp不为空。

        // 判断如果父节点是否是祖父节点的左节点

        if (xp == (xppl = xpp.left)) {

          // 父节点xp是祖父的左节点xppr

          // 判断祖父节点的右节点不为空并且是否是红色的

          // 此时xpp的左右节点都是红的,所以直接进行上面所说的第三种变换,将两个子节点变成黑色,将xpp变成红色,然后将红色节点x顺利的添加到了xp的后面。

          // 这里大家有疑问为什么将x = xpp?

          // 这是由于将xpp变成红色以后可能与xpp的父节点发生两个相连红色节点的冲突,这就又构成了第二种旋转变换,所以必须从底向上的进行变换,直到根。

          // 所以令x = xpp,然后进行下下一层循环,接着往上走。

          if ((xppr = xpp.right) != null && xppr.red) {

            xppr.red = false;

            xp.red = false;

            xpp.red = true;

            x = xpp;

          }

          // 进入到这个else里面说明。

          // 父节点xp是祖父的左节点xppr。

          / 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。 // 下面要判断x是xp的左节点还是右节点。

          else {

          // x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换需要进行两次旋转,这里先进行一次旋转。

          // 下面是第一次旋转。

            if (x == xp.right) {

              root = rotateLeft(root, x = xp);

              xpp = (xp = x.parent) == null ? null : xp.parent;

            }

          // 针对本身就是xpp左->xp左->x的结构或者由于上面的旋转造成的这种结构进行一次旋转。

          if (xp != null) {

            xp.red = false;

            if (xpp != null) {

              xpp.red = true;

              root = rotateRight(root, xpp);

            }

          }

        }

      }

      // 这里的分析方式和前面的相对称只不过全部在右测不再重复分析。

      else {

        if (xppl != null && xppl.red) {

          xppl.red = false;

          xp.red = false;

          xpp.red = true;

          x = xpp;

        }

        else {

          if (x == xp.left) {

            root = rotateRight(root, x = xp);

            xpp = (xp = x.parent) == null ? null : xp.parent;

          }

          if (xp != null) {

            xp.red = false;

            if (xpp != null) {

              xpp.red = true;

              root = rotateLeft(root, xpp);

            }

          }

        }

       }

      }

    }

    下面简述一下前面的两种种幸运的情况

    1. x本身为根节点返回x。
    2. x的父节点为黑色或者x的父节点是根节点直接返回不需要变换。

    红黑树变色规则

    • 单旋转变换。
    • 双旋转变换(需要两次反方向的单旋转)。
    • 当遇到两个子几点都为红色的话执行颜色变换,因为插入 是红色的会产生冲突。如果根节点两边的子节点都是红色,两个叶子节点变成黑色,根节点变成红色,然后再将根节点变成黑色。

    上面的图中描述了红黑树中三种典型的变换,其实前两种变换这正是AVL Tree中的两种典型的变换。

    死循环问题

    死循环问题在JDK 1.8 之前是存在的,JDK 1.8 通过增加loHead和loTail进行了修复。

    在JDK 1.7及之前 HashMap在并发情况下导致循环问题,致使服务器cpu飙升至100%,

    下面这个方法就是出现死循环的方法了

    那么当多线程(A、B线程)同时访问我们这段代码时:

    现在A线程执行到以下代码时:

    Entry<k,v> next = e.next;

    线程A交出时间片,线程B这时候接手转移并且完成了元素的转移,这个时候线程A又拿到时间片并接着执行代码:

    执行后代码如图,当e = a时,这时候这时候再执行:

    e.next = newTable[i];// a元素指向了b元素 产生循环

    这样链表就就产生了循环,在get元素的时候,线程会一直在环了遍历,无法跳出,从而导致cpu飙升!

    参考:

    https://my.oschina.net/lienson/blog/3028550

    https://www.cnblogs.com/chuonye/p/10907457.html

    https://blog.csdn.net/csdn15698845876/article/details/88408804

    https://www.cnblogs.com/coloz/p/10598122.html

    https://www.cnblogs.com/zhuoqingsen/p/8577646.html

    https://blog.csdn.net/lch_2016/article/details/81045480

    http://www.importnew.com/31278.html

    http://www.importnew.com/22011.html

    https://www.cnblogs.com/finite/p/8251587.html

    https://www.cnblogs.com/DoubleP/p/11450408.html

  • 相关阅读:
    设计模式 享元模式(池化技术)
    设计模式 混合模式(整体部分模式)
    设计模式 适配器模式
    Flex3示例、 安装 、注册码
    VS2010错误
    转载:glut.h 与 stdlib.h中 的exit()重定义问题的解决
    宿迁软件QQ群(109233721)
    百度地图 开发API接口啦
    Sublime Text 插件个人使用总结&推荐
    sublime text2 使用安装插件中文乱码问题解决
  • 原文地址:https://www.cnblogs.com/coloz/p/14343621.html
Copyright © 2011-2022 走看看