美文网首页
HashMap相关

HashMap相关

作者: 关耳木水 | 来源:发表于2018-08-15 16:43 被阅读0次

    HashMap容量为2次幂的原因


    hash方法可以计算一个对象的hashcode,我们不用过于关注

    但是他计算hashcode在bucket数组中的位置是怎么计算的呢?

    i = (n - 1) & hash

    后面就是大家熟悉的碰撞冲突拉链法解决

    这里i = (n - 1) & hash就可以实现一个均匀分布

    我们比如容量是n=32,那么32-1=31,31的二进制是11111

    用我们取得的对象的hashcode和31去进行与操作

    我们假设hashcode从1,2,3开始,分别得出的结果是

    00001 1

    00010 2

    ....

    11111 31

    发现是呈一个均匀分布的

    如果n=50呢,49的二进制是110001,再作同等假设,得出的值是

    000000 : 0
    000001 : 1
    010000 : 16
    010001 : 17
    100000 : 32
    100001 : 33
    110000 : 48

    110001 : 49

    这就是本质原因,你不用2的幂,保证不了均匀的分布

    我们正常的思维是进行一个%运算,但是%运算效率太过低下,所以采用2进制运算,同时为了保证2进制的情况下进行一个均匀分布,所以把容量设置成2的幂,这就是最正确的回答
    计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),
    有人怀疑两种运算效率差别到底有多少,我做个测试:

    public static void main(String[] args) {
        
        long currentTimeMillis = System.currentTimeMillis();
        int a=0;
        int times = 10000*10000;
        for (long i = 0; i < times; i++) {
             a=9999%1024;
        }
        long currentTimeMillis2 = System.currentTimeMillis();
        
        int b=0;
        for (long i = 0; i < times; i++) {
             b=9999&(1024-1);
        }
        
        long currentTimeMillis3 = System.currentTimeMillis();
        System.out.println(a+","+b);
        System.out.println("%: "+(currentTimeMillis2-currentTimeMillis));
        System.out.println("&: "+(currentTimeMillis3-currentTimeMillis2));
    }
    

    结果:
    783,783
    %: 359
    &: 93


    如何解决hash冲突


    • [开放定址法]
      1. [线性探测再散列]
      2. [二次探测再散列]
      3. [伪随机探测再散列]
    • [再哈希法]
    • [链地址法]
    • [建立公共溢出区]

    HashMap采用链地址法,即拉链法
    链地址法

    这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。


    resize死循环


    我们都知道HashMap初始容量大小为16,一般来说,当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash,这个成本相当的大。因为rehash,所以之前在一个位置的值,可能出现在不同的位置。

     void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
     
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            table = newTable;
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
    }
    

    大概看下transfer:

    对索引数组中的元素遍历
    对链表上的每一个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点。
    循环2,直到链表节点全部转移
    循环1,直到所有索引数组全部转移

    经过这几步,我们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了,死锁问题不就是因为1->2的同时2->1造成的吗?所以,HashMap 的死锁问题就出在这个transfer()函数上。

    假如有两个线程P1、P2,以及链表 a=》b=》null

    1、P1先运行,运行完"Entry<K,V> next = e.next;"代码后发生堵塞,或者其它情况不再运行下去,此时e=a。next=b

    2、而P2已经运行完整段代码,于是当前的新链表newTable[i]为b=》a=》null

    3、P1又继续运行"Entry<K,V> next = e.next;"之后的代码,则运行完"e=next;"后,newTable[i]为a《=》b。则造成回路,while(e!=null)一直死循环


    为什么长度为8的时候转换为红黑树


    因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3。。如果继续使用链表,平均查找长度为8/2=4。这才有转换为树的必要。。链表长度如果是6以内,6/2=3,速度也很快的。转化为树还有生成树的时间,并不明智。

    相关文章

      网友评论

          本文标题:HashMap相关

          本文链接:https://www.haomeiwen.com/subject/itywbftx.html