HashMap1.8前、后CPU load增加的原因以及部分源码解读

HashMap

1.8之前扩容方法resize()采用头插方法

如果数组元素为链表时,扩容resize方法是先将链表尾部重新rehash,插入新数组,然后插入尾部的parent,依次类推

该方法是在一个while()循环中的,仅当e == null时才能退出循环:

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;//——————(1)
                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;
            }
        }
    }

但在多线程情况下,可能存在线程A运行到(1)处时,失去时间片;线程B将该链表的扩容执行完毕。

此时由于resize()采用的是头插,如果链表中前后元素仍然插入到同一个位置,链表应有的顺序反过来了;

这时A恢复了执行,走的仍然是B线程该走的路;但此时由于顺序反过来了,但A线程并不知道,照原有的逻辑依次走下去,原来链表尾端的next将会是null的,但线程B将链表反转了,此时链表尾部变成了链表的头部,next不再是null而可能会变成原来链表中的某个元素,这样的链表,就不再会出现e==null的元素了。也就是网上说的,链表成环。

我个人觉得成环虽然是一种形象的说法,但我觉得我更能接受的说法是:由于java并没有对HashMap做并发控制(没错,不管1.8以前还是1.8以后,1.8以后只是改变了插入流程:

next = e.next;
//比较容易造成迷惑的是这里。首先不论从扩容还是从初始化上看oldCap都是2的整数次方,
//所以这里的 e.hash & oldCap
//(有点不理解是不是,你想啊:一般取余是e.hash & (oldCap - 1),这里 & 个 oldCap是什么意思?)
//那我们往高一位看,oldCap的二进制数只有一位为1(看注释第一句),
//而这为1的那位,刚好是oldCap-1(底下全为1用来判断余数的),
//所以这一位是干嘛的呢,是判断e.hash是否为 oldCap的奇数倍的。e.hash在这位为1时,才是oldCap的奇数倍
//这样在扩容两倍的时候,就会转一个圈回来,j仍然是j。
//如果是偶数倍,则不用说了,转一圈还要多出一个oldCap的偏移量
//这也就是下面数组放的位置的缘由
if ((e.hash & oldCap) == 0) {
    if (loTail == null)
        loHead = e;
    else
        //尾插本插,这就保证了不管咋并发,就往尾部塞就是了,注意啊,它插得可不是原来的e,它插的是tail
        //所以和while的终止条件while ((e = next) != null);不会产生冲突
        //而且这个是局部变量,所以怎么插都不会影响别个线程。
        loTail.next = e;
    loTail = e;
}
else {
    if (hiTail == null)
        hiHead = e;
    else
        hiTail.next = e;
    hiTail = e;
}
//loTail和hiTail这两个是根据rehash结果不同,分别存在两个不同的数组中一个下标是原来的j另外一个是j+oldCap偏移量,由于判断过原体量并不会触发treeify,所以分割后很大可能链表长度也变小了,更不会触发。
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

中间插得有点多,那我们话又说回来:由于java并没有对HashMap做并发控制,且在原有的链表元素上进行操作,导致了循环终止条件永远无法满足。造成了无法return的死循环,让CPU的load增高。

这里我只读了1.8的源码

很好,解决了HashMap链表扩容死循环问题。

那么就安全了么?

有没有听说过红黑树:java引进了红黑树,解决了Hash分布极端不均匀情况下,元素全部插到一条链表里,使HashMap退化成了一条链表,降低HashMap的查找效率,这些…都背…咳咳,都知道吧?

那么,我们来看下TreeNode吧

   */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        //..........省略
    }

好,看到这些属性没。

再看看介两过:

     /**
         * Returns root of tree containing this node.
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
        static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
            x.red = true;
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                
                //....省略
            }

以及万恶的源泉:

        /**
         * Tree version of putVal.
         */
        final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            TreeNode<K,V> root = (parent != null) ? root()/**看这里**/ : this;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    moveRootToFront(tab, balanceInsertion(root, x)/**再看这里**/);
                    return null;
                }
            }
        }

当然不止这些,查root()就可以看到:

在这里插入图片描述

很好,有put那自然就要有remove,不然像什么话嘛。

我们看看:

        static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
                                                   TreeNode<K,V> x) {
            for (TreeNode<K,V> xp, xpl, xpr;;)  {
                if (x == null || x == root)
                    return root;
                else if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }

对,不用看了,这个就是那个什么remove中调用的方法。

太 危 险 啦 !

虽然不知道并发情况下是什么样的,但我知道TreeNode里面那些元素都是可以在并发情况下发生改变的。

像什么:r.parent啊,x.parent啊,之类的,在判断的时候都是可以变的。

我看了下网上(复z制去到ρĨη⫐Ǖ℺ⓓǘὂ(划掉 google:HashMap1.8死循环)死循环拉出来 架烧烤架上的 基本是root()这个方法:

     /**
         * Returns root of tree containing this node.
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }

理解一哈,毕竟人家只有一个return,容错概率小。

像什么balanceDeletion()啊、balanceInsertion()啊;人家的return就很多。容错概率大一些。

所以一般都比较隐蔽,我看了几篇文章(我写这篇也是由于突然看到1.8以后的HashMap也会产生load100%的问题,有点颠覆常识,就决定查查资料看看原因,但好像没几篇点的让我感觉很明晰),好像只有一个兄弟点出了balanceInsertion()也会造成死循环,并指明了大致原因。(为我指明了方向,在那些下面,大部分朋友的反馈,基本就是哪怕是root()方法的死循环,复现都比较困难)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值