讨厌的ConcurrentModificationException

介绍及时失败fail-fast

fail-fast是java容器的一种错误处理机制。当迭代容器的同时,另一个线程修改了容器,就有可能抛出ConcurrentModificationException异常。

单线程中的ConcurrentModificationException

然而ConcurrentModificationException不仅可能在多线程环境下产生,也可能在单线程环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//去除list中的空元素
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {{
add("1");
add("2");
add("3");
add("");
}};
for (String element : list) {
if (element.isEmpty()) {
list.remove(element);
}
}
}

将抛出异常

1
2
3
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)

我们看到,只有一个主线程的情况下,也可能抛出ConcurrentModificationException,这跟“及时失败”的实现方式有着密切的关系。

及时失败实现方式

首先要知道,无论使用for还是foreach进行迭代,都是使用Iterator。当遍历容器调用hasNext或next时,会调用checkForComodification检查当前的modCount。

1
2
3
4
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

modCount是指容器修改计数器,即容器每进行一次修改,modCount计数器都会加1。
如果在迭代期间计数器被修改,modCount不等于迭代前期望的modCount,就会抛出ConcurrentModificationException。

单线程ConcurrentModificationException解决办法

这样单线程抛出ConcurrentModificationException的例子就容易解释了,在迭代list的同时执行了remove方法,修改了modCount值。

怎么解决?可以使用Iterator提供的remove方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {{
add("1");
add("2");
add("3");
add("");
}};
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next().toString();
if (element.isEmpty()) {
iterator.remove();
}
}
}

iterator.remove()将会重置expectedModCount,所以在下次执行iterator.hasNext()时,modCount等于expectedModCount。

iterator.remove源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

java8使用removeIf方法

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {{
add("1");
add("2");
add("3");
add("");
}};
list.removeIf(i -> i.isEmpty());
}

多线程避免ConcurrentModificationException

迭代时加锁

迭代时加锁是避免ConcurrentModificationException最简单粗暴的方法,然而牺牲了性能和可伸缩性,不推荐采用。

克隆容器

第二种方式就是克隆容器,在克隆容器上进行迭代。这种方式的好坏依赖于容器的大小,容器过大在克隆容器时,存在显著的性能开销。

使用线程安全的容器

最推荐的就是使用java原生支持的并发容器,CopyOnWriteArrayList、ConcurrentHashMap等。

隐藏的迭代

要特别注意容器中隐藏的迭代过程,toString,equals,hashCode,containsAll,removeAll,retainAll等操作会间接的迭代容器,所以这些操作也可能抛出ConcurrentModificationException。