• JDK1.8 HashMap 扩容 对链表(长度小于默认的8)处理时重新定位的过程


    关于HashMap的扩容过程,请参考源码或百度。

    我想记录的是1.8 HashMap扩容是对链表中节点的Hash计算分析.

    对术语先明确一下:

    hash计算指的确定节点在table[index]中的链表位置index,不是节点的hash值。

    1 Node<K,V> loHead = null, loTail = null; //这两个是记录重新hash计算后仍在原位置(设为index)的节点
    2 Node<K,V> hiHead = null, hiTail = null; //这两个是记录重新hash计算后在原位置加上原容量  的位置的节点(index + old capacity)

    那么问题来了 , 怎么就确定 扩容前的 链表节点  在 扩容后的位置 是 当前位置或者+old capacity的位置 ? 

    先看 put 节点时 对key查找在table位置的计算方法,在putVal中有这么一行(红色部分):

    1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    2                    boolean evict) {
    3         Node<K,V>[] tab; Node<K,V> p; int n, i;
    4         if ((tab = table) == null || (n = tab.length) == 0)
    5             n = (tab = resize()).length;
    6         if ((p = tab[i = (n - 1) & hash]) == null)
    7             tab[i] = newNode(hash, key, value, null);
    8         else {

    其中 (n-1) & hash 就是在table中的位置(n即capacity),暂时将这个记为 oldIndex = (n-1) & hash (公式1);

    再来看扩容时的代码,代码有点多,我删去一些不影响理解的还是看红色部分:

     1 final Node<K,V>[] resize() {
     2         Node<K,V>[] oldTab = table;
     3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
     4         int oldThr = threshold;
     5         int newCap, newThr = 0;
     6         if (oldCap > 0) {
     7             if (oldCap >= MAXIMUM_CAPACITY) {
     8                 threshold = Integer.MAX_VALUE;
     9                 return oldTab;
    10             }
    11             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
    12                      oldCap >= DEFAULT_INITIAL_CAPACITY)
    13                 newThr = oldThr << 1; // double threshold
    14         }26         threshold = newThr;28         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    29         table = newTab;
    30         if (oldTab != null) {
    31             for (int j = 0; j < oldCap; ++j) {
    32                 Node<K,V> e;
    33                 if ((e = oldTab[j]) != null) {
    34                     oldTab[j] = null;
    35                     if (e.next == null)
    36                         newTab[e.hash & (newCap - 1)] = e;
    37                     else if (e instanceof TreeNode)
    38                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    39                     else { // preserve order
    40                         Node<K,V> loHead = null, loTail = null;
    41                         Node<K,V> hiHead = null, hiTail = null;
    42                         Node<K,V> next;
    43                         do {
    44                             next = e.next;
    45                             if ((e.hash & oldCap) == 0) {
    46                                 if (loTail == null)
    47                                     loHead = e;
    48                                 else
    49                                     loTail.next = e;
    50                                 loTail = e;
    51                             }
    52                             else {
    53                                 if (hiTail == null)
    54                                     hiHead = e;
    55                                 else
    56                                     hiTail.next = e;
    57                                 hiTail = e;
    58                             }
    59                         } while ((e = next) != null);
    60                         if (loTail != null) {
    61                             loTail.next = null;
    62                             newTab[j] = loHead;
    63                         }
    64                         if (hiTail != null) {
    65                             hiTail.next = null;
    66                             newTab[j + oldCap] = hiHead; (公式4)
    67                         }
    68                     }
    69                 }
    70             }
    71         }
    72         return newTab;
    73     }

    为了简单(二进制可以短点)起见,以 oldCapacity = 8 为例,扩容后 capacity = 16

    newCap = oldCap << 1 : 新的容量capacity= 8<<1 = 16
    newTab[e.hash & (newCap - 1)] , 红色部分即 原节点在 新table中的位置 , 暂时记为 newIndex = e.hash & (newCap - 1) (公式2)

    hoHead,hoTail,hiHead,hiTail 不多说,上面有说明。 那么 这个下面这个判断条件,就决定了节点在扩容以后的位置

    (e.hash & oldCap) == 0  (公式3)

    最后再看是如何分配者两个链表的,一个原位置,一个j+oldCap位置。

    下面分析是如何这么确定的:

    总结上面所描述的,有下列几个变量

    oldCapacity 用 n 代替

    oldIndex = (n-1) & hash (公式1)

    newIndex = (n*2 -1) & hash (公式2)

    condition = n & hash (公式3)

    几个值之间的关系:

    if(condition == 0) 那么  newIndex 等于 oldIndex

    else                              newIndex 等于 oldIndex + n

    里面有一个隐含条件: n 是 2 的整数倍(也是满足关系的必要条件)

    当n = 8 时, 其二进制 b1 = 1000 ,  减去1 之后(7) 得到的二进制 b2 = 0111

    扩容后, 即n*2 = 16的二进制是 10000 , 减去1之后(15) 得到的二进制是 b3 = 01111

    假如hash的二进制是hash = 10111 ,

          10111 &  1000 (b1)  = 0     == conditon 

          10111 &  0111 (b2)  = 111  == oldIndex

          10111 & 01111 (b3) = 111  == newIndex

                可以发现,   当 condtion 为0 时, hash的二进制 的 第4位 必然为 0 ,高位无所谓什么值 ,

            hash &b2 (111) 结果必然是hash低三位的值,hash & b2 (1111) ,由于hash第4位为0 ,那么结果必然仍是hash低三位的值。

      所以,当condition为0时, newIndex 必然等于 oldIndex

    假如hash的二进制是hash = 11111,

        11111 & 1000 (b1) = 1000      == condition = 8

        11111 &  0111 (b2) = 00111    == oldIndex = 7

        11111 & 01111 (b3) = 01111    == newIndex = 15

         发现了吗? oldIndex 和 newIndex 差的就是 一个第 4 位的1 ,那么这个1 就是 2^4 = 8 , 也就是 oldCapacity (2^4) 的值。

    无论多长的hash值,关键的一个二进制 在 第 x 位 (2^x = oldCapacity), 也就是这一位决定了 扩容前后的位置。

    由于这样计算呢, java1.8中的HashMap 可以 不用在扩容的时候 一直往头节点插 (有环回的逻辑所以在多线程扩容的时候才会出现 闭环链 ),

    1.8中对链表只有往后添加节点,没有环回的逻辑, 也就不可能在多线程的时候出现闭环链。

    虽然时间复杂度是一样的,但是更机智了。

  • 相关阅读:
    Django的模型层(1)- 单表操作(上)
    Django-1版本的路由层、Django的视图层和模板层
    Django准备知识-web应用、http协议、web框架、Django简介
    MySQL数据库(5)- pymysql的使用、索引
    MySQL数据库(4)- 多表查询、可视化工具Navicat的使用、设计模式MVC
    MySQL练习题
    MySQL数据库(3)- 完整性约束、外键的变种、单表查询
    MySQL数据库(2)- 库的操作、表的操作、数据的操作、存储引擎的介绍
    MySQL数据库(1)- 数据库概述、MySQL的安装与配置、初始SQL语句、MySQL创建用户和授权
    前端收尾
  • 原文地址:https://www.cnblogs.com/selfchange/p/9270461.html
Copyright © 2020-2023  润新知