一、问题和背景
二、源码解读
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); if (size++ >= threshold) // 如果键值对个数超过了HashMap当前容量的阈值 resize(2 * table.length); // 调用resize()函数进行扩容 }
在这个 addEntry() 函数中,会判断键值对个数是否超过了HashMap当前容量的阈值,如果超过了,则说明需要扩容,接下来就调用 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); // 把老数组中的所有键值对都拷贝到新数组中 table = newTable; // 修改老数组的指向,把老数组指向新数组,完成扩容 threshold = (int)(newCapacity * loadFactor); }
void transfer(Entry[] newTable) { Entry[] src = table; // 老数组 int newCapacity = newTable.length; // 新数组的长度 for (int j = 0; j < src.length; j++) // 遍历老数组,把老数组中所有键值对拷贝到新数组 Entry<K,V> e = src[j]; // 记录下老数组第 j 个链表,接下来会链表上的键值对都拷贝到新数组 if (e != null) { // 如果链表不为空才需要拷贝 src[j] = null; // 先老数组第j个链表置为空链表 do { // 循环遍历刚才记录下来的链表,把所有键值对都采用头插法插入到新数组对应链表 Entry<K,V> next = e.next; // 记录下当前结点的下个结点 int i = indexFor(e.hash, newCapacity); // 求出该键值对在新数组的下标,即该键值对应该被插入到新数组第几个链表 e.next = newTable[i]; // 把结点的next指针指向新数组的第i个链表头结点 newTable[i] = e; // 新数组第i个链表的头结点前移,指向当前结点 e = next; // 把指向当前结点的指针后移 } while (e != null); } } }
do { // 循环遍历刚才记录下来的链表,把所有键值对都采用头插法插入到新数组对应链表 Entry<K,V> next = e.next; // 记录下当前结点的下个结点 int i = indexFor(e.hash, newCapacity); // 求出该键值对在新数组的下标,即该键值对应该被插入到新数组第几个链表 e.next = newTable[i]; // 把结点的next指针指向新数组的第i个链表头结点 newTable[i] = e; // 新数组第i个链表的头结点前移,指向当前结点 e = next; // 把指向当前结点的指针后移 } while (e != null);
执行上面的do while循环,第一轮循环:
第三轮也是最后一轮循环,前面已经假设结点 c 将在新数组中的第二个链表
多线程环境中扩容
四.总结
通过解读HashMap源码并结合实例可以发现,HashMap扩容导致死循环的主要原因在于扩容过程中使用头插法将oldTable中的单链表中的节点插入到newTable的单链表中,所以newTable中的单链表会倒置oldTable中的单链表。那么在多个线程同时扩容的情况下就可能导致扩容后的HashMap中存在一个有环的单链表,从而导致后续执行get操作的时候,会触发死循环,引起CPU的100%问题。所以一定要避免在并发环境下使用HashMap。