JDK1.7中HashMap死循环问题源码解读。
JDK1.7中HashMap为什么会出现死循环呢,这里我看了网上很多资料,讲的很详细,但还是准备自己再重推一下,记下笔记,以便有更深的映像
put(K key, V value)逻辑
HashMap的死循环发生在扩容方法 resize(int newCapacity) 中,每当我们往map中添加元素时,HashMap会做一次校验,如果当前HashMap中链表的长度 size 已经大于等于当前的阈值 threshold 时,便会调用 resize(int newCapacity) 方法进行扩容。
/**
* 往bucket桶中添加元素
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) { //长度校验
//扩容 此时会传入当前table数组两倍的数值当作扩容后table的长度
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//计算下标
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
resize 主要分为两步:创建新数组和将老数组中元素转移到新数组。在HashMap中,元素的下标都是通过 key 的 hash值 和 当前table长度 size 进行按位与计算取模得出的,而我们为HashMap进行扩容也就意味之我们需要重新为这些元素计算下标。
我们继续进入 resize 方法中
void resize(int newCapacity) {
...
//用传入的参数new一个table数组
Entry[] newTable = new Entry[newCapacity];
//将老数组中的元素转移到新数组中,元素下标的重新计算也在这里执行
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//将table指向扩容后的数组对象
table = newTable;
//重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer 方法
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//使用嵌套循环遍历数组 + 链表 结构中的每一个元素
for (Entry e : table) {
while(null != e) {
//因为在之后的操作中会覆盖当前元素 e 的 next 属性
//所以这里优先声明一个next变量,用于保存元素 e 的下一个节点
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新计算出元素 e 在新数组的下标 i
int i = indexFor(e.hash, newCapacity);
//使用头插法将元素插入到新数组下位为 i 的链表当中
e.next = newTable[i];
newTable[i] = e;
//将之前保存的下一节点 next 赋值给 变量 e,如果不为 null
//则再重复相同的逻辑转移该节点
e = next;
}
}
}
起初不能很好的理解transfer方法中的逻辑的话,可以尝试画张图增强理解。
死循环问题
在多线程高并发的环境下,有可能出现有多个线程同时调用扩容方法。这里假设有两个线程 线程A , 线程B 同时调用的扩容方法,并各自都创建了一个newTable
此时线程A在执行到 Entry
此时线程A 中变量引用关系:
线程A重新开始执行,在第一次while循环过后,线程A中的变量引用关系将会变成如下图所示
第二次循环后
然后到了关键的第三次循环,当代码执行到 e.next = newTable[i]; 时,我们发现 a节点 和 b节点 的 next 属性互相引用,形成了环。
而此时如果运行 get 方法,去查找该链表的话,就会进入死循环。
迷迷糊糊得做了几年CRUD工程师,期间人也比较懒散,对学习不怎么上心,过着得过且过的日子。回过头来发现自己已远远落后于他人,现在在焦虑中重新拿起书本开始学习。虽然意识到问题有点迟了,但好在还不算晚。