HashMap在并发情况下调用put的引起的死循环分析

今天在看《java并发编程的艺术》这本书的时候看到作者提到HashMap在多线程并发的环境下调用put()有可能出现死循环,导致cpu100%的现象,看了下源码结合网上的分析说明下这种可能性。可能出现问题的地方是在扩容的时候.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public V put(K key, V value)  
{
......
//算Hash值
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
//如果该key已被插入,则替换掉旧的value (链接操作)
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;
}

void addEntry(int hash, K key, V value, int bucketIndex)
{
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
if (size++ >= threshold)
//扩容
resize(2 * table.length);
}

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

resize()方法本身没有问题,问题出在transfer(newTable);这个方法是用来移动oldTable里的数据到newTable里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** 
* Transfers all entries from current table to newTable.
*/
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]; //(1)
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next; //(2)
int i = indexFor(e.hash, newCapacity);

e.next = newTable[i]; //(3)
newTable[i] = e;
e = next;
} while (e != null);
}
}
}

下面分析可能出现的情况,假设原来oldTable里存放a1,a2的hash值是一样的,那么entry链表顺序是:
P1:oldTable[i]->a1->a2->null
P2:oldTable[i]->a1->a2->null

线程P1运行到(1)这行时,e=a1(此时a1.next=a2),继续运行到(2)时,next=a2(这时候线程1获取了e 和next结点的引用)。这个时候切换到线程P2,线程P2执行完这个链表的循环,即完成整个hashMap的复制,得到一个新的hashMap。如果恰a1,a2在新的table中的hash值又是一样的,那么此时的链表顺序是:

主存:newTable[i]->a2->a1->null

注意这个时候,a1,a2连接顺序已经反了。现在cpu重新切回P1,在(3)这行以后:e.next = newTable[i];即:
a1.next=newTable[i];
newTable[i]=a1;
e=a2;//()

开始第二次while循环(e=a2,next=a1):(因为这边是引用,所以从主存中可以看出a2的next是指向a1的,这也就是后面发生循环的原因所在.这一步是将a2的next指向了a1)
a2.next=newTable[i];//也就是a2.next=a1
newTable[i]=a2
e=a1; (又获取了a1,此时a1)

开始第三次while循环(e=a1,next=null)
a1.next=newTable[i];//也就是a1.next=a2

这个时候a1.next=a2,a2.next=a1,形成回环了,这样就造成了死循环,在get操作的时候next永远不为null,造成死循环。
可以看到很偶然的情况下会出现死循环,不过一旦出现后果是非常严重的,多线程的环境还是应该用ConcurrentHashMap。

0%