HashMap 源码put()方法

Posted HaSaKing_721

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap 源码put()方法相关的知识,希望对你有一定的参考价值。

前言

关于线程安全,put方法是能够暴露出线程安全的问题的,当两个不同的hashcode经过hash计算得到同样的index时,应该构成链表,但是如果多线程放到同一个index时,有可能发生覆盖,就是在判断table[index]是否为空的时候会出现这个问题。

废话不多说直接上源码

源码

public V put(K var1, V var2) 
        return this.putVal(hash(var1), var1, var2, false, true);
    

hash()方法

    static final int hash(Object var0) 
        int var1;
        // h = key.hashCode() 计算哈希值
        // ^ (h >>> 16) 高 16 位与自身进行异或计算,保证计算出来的 hash 更加离散
        return var0 == null ? 0 : (var1 = var0.hashCode()) ^ var1 >>> 16;
    

问题:为什么哈希桶组table的长度length必须是2的n次方

比如我们设定length=16;在jdk1.8中,HashMap在put的时候会对key进行运算之后变成index就是table数组的位置,就是这队K,V要存放的位置,那么K->index,就要满足三个特点:

  1. 必须为int;
  2. 要保证足够的散列;
  3. 得到的index一定要在table数组的范围内,就是0-15;

这下要求正是对应了源码中的Hash方法,

  • 第一步:int h = key.hashCode();
  • 第二步:h ^ (h >>> 16); 这个就叫做 扰动函数
  • 第三步:(n-1) & h;

扰动函数

第二步为了降低哈希码的冲突,这样保证lenth在比较小的情况下高低Bit位都参与到Hash的计算中。hashcode无符号右移16位,高位补0,并与hashcode进行*按位异或^ 运算*。为什么要右移16位,因为看到在进行与运算的时候只有后面几位参与到了运算,所以进行右移,让他结果做到足够的分散;那为什么有要异或运算呢,这要是异或运算的一个特点,一位发生变化记过一定变化,也是为了结果足够的散列
注:异或运算,相同位不同则为1,相同则为0。00101 ^ 11100 = 11001,异或运算的特点就是有一位发生变化结果一定变化。

第三步,这里我们假设h=834957084,如果想要保证834957084在0-15之间,我们可以用取模运算:834957084 % 16,得到的结果一定是0-15之间,但是HashMap源码中选择的是*与运算*

二进制计算

16的二进制为:         0001 0000

16-1=15 二进制为:   0000 1111


0000 1111任意一个h:0110 1110 &(与运算,两个为1则为1)结果:0000 1110

最后得到的结果,二进制最小值一定是0000,最大值一定是1111,即十进制:0-15,这就是保证了上面第三步的范围,其实与取模运算是一样的;但是为什么选择了&与运算,因为&与运算是位运算,效率要比取模%高很多

再说第二步,通过上面的运算,我们发现,h的高4位是没有参与运算的,那么要想让高4位参与到运算过程中,就要右移,h右移:0000 0110,再进行异或操作,就能够让散列性更好,对应第二步

putVal()方法

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) 
    Node<K,V>[] tab; // tables 数组
    Node<K,V> p; // 对应位置的 Node 节点
    int n; // 数组大小
    int i; // 对应的 table 的位置
    //  如果 table 未初始化,或者容量为 0 ,则进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果对应位置的 Node 节点为空,则直接创建 Node 节点即可。
    if ((p = tab[i = (n - 1) & hash] /*获得对应位置的 Node 节点*/) == null)
        tab[i] = newNode(hash, key, value, null);
    // 如果对应位置的 Node 节点非空,则可能存在哈希冲突
    else  //Hash冲突逻辑
        Node<K,V> e; // key 在 HashMap 对应的老节点
        K k;
        // 如果找到的 p 节点,就是要找的,则则直接使用即可
        if (p.hash == hash && // 判断 hash 值相等
            ((k = p.key) == key || (key != null && key.equals(k)))) // 判断 key 真正相等
            e = p;
        // 如果找到的 p 节点,是红黑树 Node 节点,则直接添加到树中
        else if (p instanceof TreeNode)//红黑树逻辑
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //  如果找到的 p 是 Node 节点,则说明是链表,需要遍历查找
        else //链表逻辑
            // 顺序遍历链表
            for (int binCount = 0; ; ++binCount) 
                // `(e = p.next)`:e 指向下一个节点,因为上面我们已经判断了最开始的 p 节点。
                // 如果已经遍历到链表的尾巴,则说明 key 在 HashMap 中不存在,则需要创建
                if ((e = p.next) == null) 
                    // 创建新的 Node 节点
                    p.next = newNode(hash, key, value, null);
                    // 链表的长度如果数量达到 TREEIFY_THRESHOLD(8)时,则进行树化。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break; // 结束
                
                // 如果遍历的 e 节点,就是要找的,则则直接使用即可
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 结束
                // p 指向下一个节点
                p = e;
            
        
        //如果找到了对应的节点
        if (e != null)  // existing mapping for key
            V oldValue = e.value;
            // 修改节点的 value ,如果允许修改
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 节点被访问的回调
            afterNodeAccess(e);
            // 返回老的值
            return oldValue;
        
    
    // <4.2>
    // 增加修改次数
    ++modCount;
    // 如果超过阀值,则进行扩容
    if (++size > threshold)
        resize();
    // 添加节点后的回调
    afterNodeInsertion(evict);
    // 返回 null
    return null;

附流程图

以上是关于HashMap 源码put()方法的主要内容,如果未能解决你的问题,请参考以下文章

HashMap源码之put()方法

源码分析——HashMap的put,resize,containskey方法原理分析

源码分析——HashMap的put,resize,containskey方法原理分析

hashmap的put方法源码分析

HashMap的Put方法源码

HashMap中 get(K key) 和 put(K key,V value) 的具体过程