HashMap死循环

HashMap的死循环【JDK1.7.0_79】

车祸现场

HashMap本身非线程安全,当将其作为全局变量,高并发场景下进行put、get、remove等操作的时候CPU会爆满,业务无响应。

源码分析

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        //计算key的hash值
        int hash = hash(key);
        //定位key在HashMap中的位置
        int i = indexFor(hash, table.length);
        //如果该位置存在值,遍历链表,存在相同键则覆盖
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //key值不同或者该位置不存在值,进入添加新节点方法
        addEntry(hash, key, value, i);
        return null;
    }

假设全局变量HashMap容量为4,当前扩容阈值为2,a和b两个key不同、hash值相同组成的一个链表结构。当线程Thread1、Thread2同时做put操作,默认a和b的hash值和HashMap中现有hash值均不冲突,此时均进入添加新节点addEntry()方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGHkswOk-1578280925463)在这里插入图片描述

    void addEntry(int hash, K key, V value, int bucketIndex) {
        //超过扩容临界值
        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);
    }

因为现在扩容阈值是2,添加新节点时候先进行扩容操作resize()。

    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);
    }

创建本线程可见的新Entry数组,通过transfer()方法将旧数组数据迁移到新数组,而a、b的hash值恰巧又相同再次形成链表结构。

    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;	----案发现场
                //迁移数据不一定重新计算hash值
                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;
            }
        }
    }

Thread1执行到“案发现场”时候,e = a、next = e.next = b,此时CPU时间片用尽;Thread2获得CPU时间片后,数组扩大为之前的一倍,将旧数组数据迁移到新数组并将新值插入数组,最后把本线程可见的数组写入内存中。执行结束,Thread1获得时间片继续执行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t3Cb9r9e-1578280925465)在这里插入图片描述

Thread1第一次循环结束,本线程可见新数组如下图所示。因为在将a赋值到新数组newTable[i]位置之前,newTable[i]为null所以e.next为null。

 		while(null != e) {				
 			Entry<K,V> next = e.next;	--案发现场 next = b、e = a
         	      //迁移数据不一定重新计算hash值
         	      if (rehash) {
         	          e.hash = null == e.key ? 0 : hash(e.key);
         	      }
         	      int i = indexFor(e.hash, newCapacity);
         	      e.next = newTable[i]; 			e.next = null
         	      newTable[i] = e;				newTable[i] = a
         	      e = next;						e = b
         	   }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7YTYpOIB-1578280925466)在这里插入图片描述

Thread1执行第二次循环,此时(e = b) != null,此时线程读取到的b.next为Thread2刷入内存中的新数组内容,next = b.next = a。

 		while(null != e) {				
 			Entry<K,V> next = e.next;	--案发现场 next = a、e = b
                //迁移数据不一定重新计算hash值
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i]; 			e.next = a
                newTable[i] = e;				newTable[i] = b
                e = next;						e = a
             }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EhBUJfNa-1578280925466)在这里插入图片描述

Thread1执行第三次循环,此时(e = a) != null,此时线程读取到的next = a.next = null。newTable[i] = b,此时a.next被赋值为b形成死循环。

 		while(null != e) {				
 			Entry<K,V> next = e.next;	--案发现场 next = null、e = a
             //迁移数据不一定重新计算hash值
             if (rehash) {
                 e.hash = null == e.key ? 0 : hash(e.key);
             }
             int i = indexFor(e.hash, newCapacity);
             e.next = newTable[i]; 			e.next = b
             newTable[i] = e;					newTable[i] = a
             e = next;							e = null
          }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JNL5TD4G-1578280925468)在这里插入图片描述

当通过get()方法获取死循环中键值对时候,线程陷入死循环链表遍历e.next一直不为null,造成死循环。

 		public V get(Object key) {
 		   if (key == null)
 		       return getForNullKey();
 		    Entry<K,V> entry = getEntry(key);
 		    return null == entry ? null : entry.getValue();
 		}
 
 		final Entry<K,V> getEntry(Object key) {
 		     if (size == 0) {
 		         return null;
 		     }
 		     int hash = (key == null) ? 0 : hash(key);
 		     //当出现死循环时候,如果查询hash值相同而key不同
 		     for (Entry<K,V> e = table[indexFor(hash, table.length)];
 		          e != null;
 		          e = e.next) {		-- 进入死循环链表e.next永远不为null
 		         Object k;
 		         if (e.hash == hash &&
 		             ((k = e.key) == key || (key != null &&
 		                                     key.equals(k))))
 		             return e;
 		     }
 		     return null;
 		 }

总结

JDK8之前HashMap之所以会产生死循环,主要是原因是transfer()方法在迁移链表数据时候将链表数据顺序倒置所致。JDK8之后在扩容之后通过head和tail机制保证链表数据顺序不变。

 						// preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    

但是新加入的红黑树机制还可能引起死循环,所以并发场景下还是用ConcurrentHashMap。JDK8之前官方文档都就说啦:高并发场景下,必须上锁,推荐使用Collections.synchronizedMap(new HashMap(…))哦。

 * <p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access a hash map concurrently, and at least one of
 * the threads modifies the map structurally, it <i>must</i> be
 * synchronized externally.  (A structural modification is any operation
 * that adds or deletes one or more mappings; merely changing the value
 * associated with a key that an instance already contains is not a
 * structural modification.)  This is typically accomplished by
 * synchronizing on some object that naturally encapsulates the map.

联系我

在这里插入图片描述在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值