JDK1.8前多线程并发下HashMap会发生死循环

在JDK1.8之前的版本中,HashMap的底层实现是数组+链表。当调用HashMap的put方法添加元素时,如果新元素的hash值或key在原Map中不存在,会检查容量size有没有超过设定的threshold,如果超过则需要进行扩容,扩容的容量是原数组的两倍,具体代码如下:

    void addEntry(int hash, K key, V value, int bucketIndex) {

//检查容量是否超过threshold

        if ((size >= threshold) && (null != table[bucketIndex])) {

//扩容

            resize(2 * table.length);

            hash = (null != key) ? hash(key) : 0;

            bucketIndex = indexFor(hash, table.length);

        }

        createEntry(hash, key, value, bucketIndex);

    }

扩容就是新建Entry数组,并将原Map中元素重新计算hash值,然后存到新数组中,具体代码如下:

 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 e : table) {

            while (null != e) {

                Entry 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;

            }

        }

    }

假设一个HashMap的初始容量是4,使用默认负载因子0.75,有三个元素通过Hash算法计算出的数组下标都是2,但是key值都不同,分别是a1、a2、a3:

 

假设插入的第四个元素a4,通过Hash算法计算出的数组下标也是2,当插入时则需要扩容,此时有两个线程T1、T2同时插入a4,则T1、T2同时进行扩容操作,它们各自新建了一个Entry数组newTable。

 

T2线程执行到transfer方法的Entry next = e.next;时被挂起,T1线程执行transfer方法后Entry数组:

 

在T1线程返回新建Entry数组复制给table之后,T2线程恢复,因为在T2挂起时,变量e指向的是a1,变量next指向的是a2,所以在T2恢复执行完transfer之后:

 

可以看到在T2执行完transfer方法后,a1元素和a2元素形成了循环引用,当调用get方法获取该位置的元素时就会发生死循环,更严重会导致CPU占用100%故障。

另外,indexFor方法理论依据:a%b==a&(b-1) 当b是2的指数时等式成立。

JDK1.8后,除了对hashmap增加红黑树结果外,对原有造成死锁的关键原因点(新table复制在头端添加元素)改进为依次在末端添加新的元素。虽然JDK1.8后添加红黑树改进了链表过长查询遍历慢问题和resize时出现导致put死循环的bug,但还是非线性安全的,比如数据丢失等等。因此多线程情况下还是建议使用concurrenthashmap。

参考文献:

http://baijiahao.baidu.com/s?id=1596234357266726808&wfr=spider&for=pc

https://blog.csdn.net/linsongbin1/article/details/54708694

https://blog.csdn.net/qq_27007251/article/details/71403647

 
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值