HashMap的ConcurrentModificationException

记录下在工作中发生的一个异常,深入源码分析观察原因,以及在之后写代码的过程中如何避免。具体的报错如下:

1
2
3
4
5
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
at java.util.HashMap$EntryIterator.next(HashMap.java:1463)
at java.util.HashMap$EntryIterator.next(HashMap.java:1461)
at com.souche.JsonTest.HashMapTest.main(HashMapTest.java:16)

ConcurrentModificationException产生原因

定位到最主要的一段代码,报错是在HashIterator中暴出来的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}

其中modCount是HashMap中实际存在的元素节点数量,而expectedModCount是复制出来的迭代器中的节点数量。来看下我们产生报错的代码:

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
package com.souche.JsonTest;

import com.alibaba.fastjson.JSONObject;

import java.util.Map;

/**
* @autor yeqiaozhu.
* @date 2019-10-30
*/
public class HashMapTest {
public static void main(String[] args) {
JSONObject jsonObject=new JSONObject();
jsonObject.put("receiver.mobile","test");
jsonObject.put("test",2);
for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
if (entry.getKey().contains(".")) {
String[] strings=entry.getKey().split("\\.");
String key=strings[0];
Object value=entry.getValue();
if (jsonObject.containsKey(key)) {
JSONObject getJSON=(JSONObject) jsonObject.get(key);
getJSON.put(key,value);
}else {
JSONObject newJSON=new JSONObject();
String newKey=strings[1];
newJSON.put(newKey,value);
jsonObject.put(key,newJSON);
}
}
//最关键的一步
jsonObject.remove(entry.getKey());
}
}
}

通过jsonObject.remove()函数我们减少了modCount的值,但是expectedModCount的值没有变。那么迭代器在继续进行下一个元素迭代的时候会报错。值得注意的是,如果先通过jsonObject.put()新增一个元素,然后又通过jsonObject.remove移除一个元素,是不会报这个错的,因为这里判断仍然相等,但是实际上是被修改了的。我们可以看一下上面描诉的效果:

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
package com.souche.JsonTest;

import com.alibaba.fastjson.JSONObject;

import java.util.Map;

/**
* @autor yeqiaozhu.
* @date 2019-10-30
*/
public class HashMapTest {
public static void main(String[] args) {
JSONObject jsonObject=new JSONObject();
jsonObject.put("receiver.mobile","test");
jsonObject.put("test",2);
for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
if (entry.getKey().contains(".")) {
String[] strings=entry.getKey().split("\\.");
String key=strings[0];
Object value=entry.getValue();
if (jsonObject.containsKey(key)) {
JSONObject getJSON=(JSONObject) jsonObject.get(key);
getJSON.put(key,value);
}else {
JSONObject newJSON=new JSONObject();
String newKey=strings[1];
newJSON.put(newKey,value);
jsonObject.put(key,newJSON);
}
//将这步移到这里就不会报错
jsonObject.remove(entry.getKey());
}
}
}
}

正确遍历HashMap(Iterator/ConcurrentHashMap)

正常情况下我们获取到迭代器之后,需要通过迭代器去操作,防止发生上面我们介绍的异常。上面的代码改成迭代器的版本。

1
2
3
4
5
6
7
8
public void usingIterator(JSONObject json){
Iterator<Map.Entry<String,Object>> iterator=json.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String,Object> entry=iterator.next();
System.out.println("key:"+entry.getKey()+" "+"value:"+entry.getValue());
iterator.remove();
}
}

我们通过迭代器去删除的时候进行expectedModCount–同时会执行modCount–操作,不会报错。当然如果我们使用ConcurrentHashMap也可以避免这个异常。

1
2
3
4
5
6
7
8
9
10
public void usingConcurrentHashMap(JSONObject jsonObject){
Map<String,Object> conMap=new ConcurrentHashMap<>();
//这里需要注意ConcurrentHashMap value不允许为空值,空值会报空指针的错误
conMap.putAll(jsonObject);
for (Map.Entry<String, Object> entry : conMap.entrySet()) {
conMap.put("key:"+entry.getKey(),"value:"+entry.getValue());
System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());
conMap.remove(entry.getKey());
}
}

通过ConcurrentHashMap不会发生ConcurrentModificationException异常。注意ConcurrentHashMap的value不允许为空值。