HashMap源码探究(死锁/扩容)【JDK1.7】【JDK1.8】

先说HashMap最重要的一点:缺点

       HashMap的缺点我们大都听说过,其在高并发的情况下表现较差,会出现一些奇奇怪怪的问题,比如使CPU使用率提高到100%此处打个小差,因为前几天,我的服务器莫名其妙CPU占用率也达到了100%,我还以为是跑了哪个项目写的有问题了,后来查了一下所有进程才发现有个ddgs的一直在高占用,经过研究发现,这是一个新型的挖矿病毒,中毒原因是我之前练习redis的时候忘了设置密码o(╥﹏╥)o),那么它为什么会出现这个原因呢?其实这个高并发下的问题,也和HashMap一个长久以来的缺点相挂钩,没错,就是HashMap 的扩容机制。

        为什么说HashMap的扩容机制是长久以来的缺点,我们可以简单看一下其源码,可知:其初始化长度为16,扩容因子为0.75(即当内容达到百分之七十五的时候会扩容为当前的二倍),那么问题其实就在于HashMap是怎样进行扩容的。

          它在扩容的时候,会先生成一个新的HashMap,然后把原HashMap里的数据一个一个的复制到新的HashMap里,那么就很轻易的知道了,当我们的数据量过大的时候,我们的HashMap会进行多次扩容,那么就会相对来说很消耗我们的资源,解决这个缺点的办法也很简单,先大致预估一下我们需要在这个HashMap里存放多少数据,然后在初始化它的时候给它先把默认长度给设置了,这样就避免了多次扩容多次复制。

          介绍完了扩容机制,那么其在高并发下那个100%的问题是怎么来的呢,不难想出,上述步骤中,最有可能出现问题的,就是复制的那一步。

          

//扩容的方法
    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];
//在此进行我们所说的复制的那一步  传入的参数为新HashMap, 初始化hash掩码(此处不太懂也没事)
        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循环是我们问题的关键
            while(null != e) {
    /*当我们遍历到一个不为空的老数据的时候,我们假定这个老数据在横向(我们知
道HashMap是由数组和链表构成的,那么假设一个二维空间里HashMap纵向是数组结构,横向是链表结构)挂载这有下一个节点,
那么我们现在想移动这个老数据,必须得保证我们下边节点的数据不丢失,所以我们创建一个next先去"指向"它*/
                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方法中,当我们看到

 Entry<K,V> next = e.next;

的时候,我们就应该有这样一个担忧,在高并发的情况下,这一步会不会影响我们的扩容,答案是肯定的……

但是说这是不是一个BUG ?  并不 

sun公司的负责人表达的意思是:我们设计HashMap本来的作用就不应该是应对高并发情况的,在高并发的情况下,我们有另外一个更好用的ConcurrentHashMap 去应对。

HashMap的put方法注意点

我们通常会简单认为HashMap的put方法的Key只进行一次hash运算,但事实上,HashMap的put实现是在计算key的hash之上,又进行了一次自己规定的位运算,以JDK1.8中源码为例(版本有差距,不过也都是进行了二次位运算)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }  



     static final int hash(Object key) {
        int h;                                        //^异或运算  >>>为带符号右移
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

猜你喜欢

转载自blog.csdn.net/qq_41173453/article/details/81185325
今日推荐