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

posted @ 2021-01-29 10:37  将军上座  阅读(69)  评论(0编辑  收藏  举报