HashMap之环形链表源码图解
Java7在多线程操作HashMap时,并发扩容可能会导致环形链表,get方法时则可能出现死循环。以下是环形链表的具体产生过程。
1.扩容逻辑实现transfer()方法:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
// 循环数组中元素,依次从copy到newTable
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 注意这句代码很重要,头插发扩容,会将元素e扩容到newTable的头位置,已有的newTable[i]会放到e的next。
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
2.扩容前的HashMap数据:
3.线程一开始扩容,注意这句关键代码:
Entry<K,V> next = e.next;
假如线程一执行完这句代码时,就被挂起了,此时线程一transfer()方法中:e = entry1, next = entry0, entry0.next = null。
4.巧了,此时线程二也开始触发扩容:
当线程二扩容完成之后,假如扩容后entry0和entry1也落到newTable的同一index位置,此时线程二中的newTable数据为:
注意, 重点来了,线程二扩容完成后之后,ertry0的next指向了entry1。
5.回到线程一继续扩容:
注意,此时线程一transfer()方法中:e = entry1, next = entry0, entry0.next = entry1。当第一个元素entry1扩容完成之后,此时线程一中的newTable数据为:
此时 e = entry0, next = entry1, entry1.next = null。
6.线程一继续将e = entry0扩容copy到newTable中:
当元素entry0扩容完成之后,此时线程一中的newTable数据为:
此时 e = entry1, next = null。
7.线程一继续将e = entry1扩容到newTable中:
当元素entry1扩容完成之后,此时线程一中的newTable数据为:
注意, 当这一轮扩容完成后,会把entry1.next = entry0,线程一扩容完成,同时线程二扩完容的时候,已经将entry0.next = entry1了,形成环形链表。
8.当调用get()方法的时候,代码逻辑:
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
// 问题就在这里,当key进行hash运算之后,刚好落在环形链表这个位置,
// 当key没有匹配,会向下一直循环e.next,然后就在环形链表中出不来了。
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}