HashMap在设计之初并没有考虑多线程并发的情况,多线程并发的情况下理论上应该使用ConcurrentHashMap,但是程序中经常会无意中在多并发的情况下使用了HashMap,如果是jdk1.8以下的版本,有可能会导致死循环,打满cpu占用,下面基于jdk1.7源码分析下原因。
我们从put看起
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry 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++;
addEntry(hash, key, value, i);
return null;
}
addEntry方法长这样:
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);
}
下面就是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);
}
然后就是关键方法transfer:
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;
}
}
}
假设原本是A->B->C的链表,旧链表为null,则第一次之后旧链表会变成B->C,新链表为A。
第二次旧链表变成C,新链表为B->A。
第三次旧链表为null,新链表为C->B->A,整个链表的顺序被颠倒。
Entry next = e.next;
这句时,发生线程切换,thread2开始扩容,此时对线程A来说,存在以下关系。thread1:
e=A
next=B
旧链表:
A->B->null
新链表:
null
thread1:
e=A
next=B
旧链表:
null
新链表:
B->A->null
e.next = newTable[i];
,执行完变成如下关系:thread1:
e=A
next=B
旧链表:
null
新链表:
B->A->B(出现循环)
newTable[i] = e;
,执行完变成如下关系:thread1:
e=A
next=B
旧链表:
null
新链表:
A->B->A
e = next;
,执行完变成如下关系:thread1:
e=B
next=B
旧链表:
null
新链表:
A->B->A
那么对于jdk1.8是如何做的呢,1.8的resize比较复杂,核心点在下面:
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node 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);