阅读 134

HashMap实现原理

HashMap实现原理-1

HashMap结构图

  • JDK1.8 HashMap的数据结构为:数组+链表+红黑树

image.png

变量介绍

  • DEFAULT_INITIAL_CAPACITY Table数组的初始化长度
// The default initial capacity - MUST be a power of two.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
复制代码
  • MAXIMUM_CAPACITY Table数组的最大长度
// The maximum capacity, used if a higher value is implicitly specified by either 
// of the constructors with arguments. MUST be a power of two <= 1<<30.
static final int MAXIMUM_CAPACITY = 1 << 30;
复制代码
  • DEFAULT_LOAD_FACTOR 负载因子:默认值为0.75。 当元素的总个数>(当前数组的长度*负载因子)。数组会进行扩容,扩容为原来的两倍
// The load factor used when none specified in constructor.
static final float DEFAULT_LOAD_FACTOR = 0.75f;
复制代码
  • TREEIFY_THRESHOLD 链表树化阙值: 默认值为 8 。表示在一个node(Table)节点下的值的个数大于8时候,会将链表转换成为红黑树
//The bin count threshold for using a tree rather than list for a bin. 
//Bins are converted to trees when adding an element to a bin with at least
//this many nodes. The value must be greater than 2 and should be at 
//least 8 to mesh with assumptions in tree removal about conversion back 
//to plain bins upon shrinkage.
static final int TREEIFY_THRESHOLD = 8;
复制代码
  • UNTREEIFY_THRESHOLD 红黑树链化阙值: 默认值为 6 。 表示在进行扩容期间,单个Node节点下的红黑树节点的个数小于6时候,会将红黑树转化成为链表
//The bin count threshold for untreeifying a (split) bin during a resize
//operation. Should be less than TREEIFY_THRESHOLD, and at most 6 
//to mesh with shrinkage detection under removal.
static final int UNTREEIFY_THRESHOLD = 6;
复制代码
  • MIN_TREEIFY_CAPACITY = 64 最小树化阈值,当Table所有元素超过改值,才会进行树化(为了防止前期阶段频繁扩容和树化过程冲突)
//The smallest table capacity for which bins may be treeified. 
//(Otherwise the table is resized if too many nodes in a bin.)
//Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts between 
//resizing and treeification thresholds.
static final int MIN_TREEIFY_CAPACITY = 64;
复制代码

HashMap.put(K,V)流程及分析

HashMap在put(K,V)流程如下:

image.png

    首先数组的查询效率高,可以通过下标直接查找,使用链表是为了解决hash冲突问题,根据上述流程图可知,在put元素时,首先通过hash(key)算法取到key的hash值,然后通过hash(key)&(n-1)可以计算出此key对应在Node[]的位置,那么就有可能两个不同的key落在同一下标位置上,存放这一类下标相同的结构为链表。链表结构如下:

// Node类 存放元素结构 JDK1.7使用的是Entry类
static class Node<K,V> implements Map.Entry<K,V> {
    // hash值
    final int hash;
    final K key;
    V value;
    // 下一个元素指针
    Node<K,V> next;
    // 构造方法
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }   // 返回 与 此项 对应的键
    public final V getValue()      { return value; } // 返回 与 此项 对应的值
    public final String toString() { return key + "=" + value; }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    /** 
    * hashCode() 
    */
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    /** 
    * equals()
    * 作用:判断2个Entry是否相等,必须key和value都相等,才返回true  
    */
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}
复制代码

分析1:hash(key)

源码如下:

// 将key转换成哈希码(hashCode)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
// 1. 取hashCode值: h = key.hashCode() 
// 2. 高位参与低位的运算:h ^ (h >>> 16)
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    // a. 当key = null时,hash值 = 0,所以HashMap的key 可为null
    // b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),
    // 然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
}
复制代码
  • 计算示意图

    hashCode为int类型,在32位操作系统占4个字节即32位,所以采用右移16位按位异或运算的方式,这样加大了hash值的随机性,从而使在后续的计算下标时,能够使下标更加均匀,充分利用空间 image.png

分析2:计算存储位置-(length - 1) & hash

    根据hash计算位置,可以用hash % length 取余数的方式来计算,但是hashMap中使用的是 & 运算来计算,可以达到同样的效果并且计算更快,举个例子:

image.png

数组长度设置为2的n次幂,
二进制为:0000 0000 0000 0000 0000 0000 0000 0001 0000
则&运算用到的值为长度-1,
二进制为:0000 0000 0000 0000 0000 0000 0000 0000 1111
通过这样的值和其他数做&运算操作时,能保证所得数字一定小于等于这个值,并且能保证小于这个数的每个值都有可能得到,保证了数组下标的使用,并且通过和上面👆处理过的hash值操作,能较少碰撞,尽量把数据分配均匀,每个链表⻓度⼤致相同。

分析3: putVal(hash(key), key, value, false, true)

// 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // n为数组长度,i为hash通过&运算获取到的数组下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1. 若哈希表的数组tab为空,则 通过resize() 创建
    // 所以,初始化哈希表的时机 = 第1次调用put函数时,即调用resize() 初始化创建
    // 关于resize()的源码分析将在下面讲解扩容时详细分析,此处先跳过
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.若哈希表的数组i位置是空,则直接在该数组位置新建节点,插入完毕
    // p 为tab[i]元素的指针
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 否则,代表存在Hash冲突,即当前存储位置已存在节点,则依次往下判断
    else {
        Node<K,V> e; K k;
        //a. 当前位置的key是否与需插入的key相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 判断需插入的数据结构是否为红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 否则是链表结构
        else {
            // 使用尾插法将新节点加入链表 
            // 为什么不用头插法,我认为是因为JDK1.8加入了红黑树的数据结构,
            // 需要判断链表结构的个数是否达到树化阀值,所以需要遍历整个链表,
            // 既然一定要遍历整个链表,不如使用尾插法。
            for (int binCount = 0; ; ++binCount) {
                // 当前位置是否是链表的尾,e为下一个位置的Node
                if ((e = p.next) == null) {
                    // 如果是尾节点,则新建节点,放在p节点后面,完成尾插
                    p.next = newNode(hash, key, value, null);
                    // 判断是否达到树化阀值,达到则进行树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在链表中存在key与需插入的key相同,返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 将p重新设置为循环的判断节点Node
                p = e;
            }
        }
        // 哈希表中已经存在相同key的Node对象了,替换Node的Value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // LinkedHashMap使用
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 修改次数加1 此处为ConcurrentHashMap的操作用
    ++modCount;
    // 如果size比阀值大,则扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
复制代码

分析4: resize()

    /**
     * 数组扩容
     * @return 扩容后的数组
     */
    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;
            }
            // 否则 将旧数组长度翻倍 并且判断旧数组长度是否比16大
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY) {
                // 将新的阀值翻倍:16*0.75=12 32*0.75=24
                newThr = oldThr << 1;
            }
        } else if (oldThr > 0) {
            // 将数组长度设置为阀值
            newCap = oldThr;
        } else {
            // 如果未设置扩容条件 则使用默认的扩容条件和容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果新阀值是0:说明旧阀值>0,但旧数组长度为0
        // 重新设置新阀值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        Node<K,V>[] newTab = new Node[newCap];
        // 新数组
        table = newTab;
        if (oldTab != null) {
            // 如果旧数组不是空 则遍历旧数组上的Node 重新挪动到新数组中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // e.next 是空 说明当前数组下标下只有一个元素 不是链表或红黑树
                    if (e.next == null) {
                        // e.hash & (newCap - 1)
                        // 相当于用e的hash值对新数组长度取余获取对应下标位置
                        // 放入新数组对应下标上
                        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;
                            // 原始Entry通过hashCode & (newCap - 1)只会落在两个下标下
                            // No1、原下标
                            // No2、原下标 << 1
                            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) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
复制代码

扩容过程中,高低链表操作详解:

image.png

分析5:尾插法+高低链表解决JDK1.7扩容环形链表问题

JDK1.7扩容源码:

/**
* 分析1:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/ 
   void resize(int newCapacity) {  
    
    // 1. 保存旧数组(old table) 
    Entry[] oldTable = table;  

    // 2. 保存旧容量(old capacity ),即数组长度
    int oldCapacity = oldTable.length; 

    // 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出    
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;  
        return;  
    }  
  
    // 4. 根据新容量(2倍容量)新建1个数组,即新table  
    Entry[] newTable = new Entry[newCapacity];  

    // 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 
    transfer(newTable); 

    // 6. 新数组table引用到HashMap的table属性上
    table = newTable;  

    // 7. 重新设置阈值  
    threshold = (int)(newCapacity * loadFactor); 
} 

 /**
   * 分析1.1:transfer(newTable); 
   * 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
   * 过程:按旧链表的正序遍历链表、在新链表的头部依次插入
   */ 
void transfer(Entry[] newTable) {
      // 1. src引用了旧数组
      Entry[] src = table; 

      // 2. 获取新数组的大小 = 获取新容量大小                 
      int newCapacity = newTable.length;

      // 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
      for (int j = 0; j < src.length; j++) { 
          // 3.1 取得旧数组的每个元素  
          Entry<K,V> e = src[j];           
          if (e != null) {
              // 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
              src[j] = null; 
              do { 
                  // 3.3 遍历 以该数组元素为首 的链表
                  // 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
                  Entry<K,V> next = e.next; 
                 // 3.4 重新计算每个元素的存储位置
                 int i = indexFor(e.hash, newCapacity); 
                 // 3.5 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
                 // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
                 e.next = newTable[i]; 
                 newTable[i] = e;  
                 // 3.6 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
                 e = next;             
             } while (e != null);
             // 如此不断循环,直到遍历完数组上的所有数据元素
         }
     }
 }
复制代码

在JDK1.7中,链表的创建使用的是头插法,并且没有用高低链表,在高并发情况下,有可能有两个线程同时操作同一个HashMap中数组下标相同的链表。两个线程之间,通过CPU调度轮换执行,容易形成环形链表,如下图所示:

image.png

文章分类
后端
文章标签