CodeXiaoMai

CodeXiaoMai的博客


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

ConcurrentModificationException 异常分析与解决方法

发表于 2019-07-28 更新于 2019-08-05 分类于 Java , 集合 阅读次数:
本文字数: 9.6k

本文主要从源码的角度分析 ConcurrentModificationException 发生的原因,以及解决办法。

需求与实现

需求:从一个集合中找出指定的元素并将其删除。

问题代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ArrayList<Integer> list = new ArrayList<>();

for (int i = 0; i < 10; i++) {
list.add(i);
}

list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
if (integer == 5) {
list.remove(integer);
}
}
});

异常堆栈:

1
2
3
4
5
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList.forEach(ArrayList.java:1260)
...

Process finished with exit code 1

原因排查

从异常的堆栈信息中可以看出,在 ArrayList 的 forEach() 方法中抛出异常,那就先看一下这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;

// 循环体内代码执行需要满足的条件有两个:
// 1. modCount == expectedModCount
// 2. i < size
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

经过对 forEach() 方法的源码分析,发现 modCount != expectedModCount 会导致 for 循环退出,并且抛出 ConcurrentModificationException。

从第 3 行代码 final int expectedModCount = modCount 中可以知道,初始状态 expectedModCount 和 modCount 的值是相等的,那么是什么原因导致它们的值不相等进而抛出异常的呢?

原因是 ArrayList 的修改操作(remove 或 add)会导致 modCount 的值发生变化。这里以 remove 方法为例进行分析证明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

读完上面的源码会发现 remove 方法并没有修改 modCount 的值啊。别着急,再仔细观察会发现无论怎样,最终会执行 fastRemove 方法。再来看一下这个方法的实现:

1
2
3
4
5
6
7
8
9
private void fastRemove(int index) {
// 修改 modCount
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}

这次没错了吧。

所以在 forEach() 方法中使用 ArrayList.remove() 删除元素会抛出异常的原因是:由于 ArrayList 执行 remove 方法后,modCount 的值加 1,当 forEach 中的循环检查两个条件时,会因 modCount == expectedModCount 不成立导致 for 循环退出,进而抛出 ConcurrentModificationException 异常。

由此可见,forEach 循环只适用于对 ArrayList 的遍历,但是不可以对 ArrayList 进行添加或删除操作,否则将抛出异常。

现在问题的原因找到了,那么我们应该怎么解决呢?

解决办法

方法一:使用普通的 for 循环

1
2
3
4
5
6
7
8
9
10
11
ArrayList<Integer> list = new ArrayList<>();

for (int i = 0; i < 10; i++) {
list.add(i);
}

for (int i = 0; i < list.size(); i++) {
if (i == 5) {
list.remove(i);
}
}

运行代码,发现没有抛出异常。问题解决!

但是,如果需求改为删除所有元素呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
ArrayList<Integer> list = new ArrayList<>();

for (int i = 0; i < 10; i++) {
list.add(i);
}

for (int i = 0; i < list.size(); i++) {
list.remove(i);
}

int size = list.size();

System.out.println("执行完毕 list.size() = " + size);

先猜一下运行结果是什么,再往下看。

——– 这里是分隔线(这里先插播一条广告,广告之后更加精彩!哈哈!!!)————

你还在为开发中频繁切换环境打包而烦恼吗?
快来试试 Environment Switcher 吧!
使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。
https://github.com/CodeXiaoMai/EnvironmentSwitcher
目前该项目已经达成如下成就:
GitHub stars GitHub forks GitHub watchers

——————- 这里是分隔线(广告结束,精彩继续!!!)——————–

好了,回归正题你猜出结果了吗?看看你猜的对下对。

运行结果为:

1
执行完毕 list.size() = 5

咦?明明是遍历 list 的每个元素并把它们删除,为什么最后还有 5 个元素呢?

这是因为 ArrayList 内部使用数组存储数据,10 个元素依次存储在数组的第 0 个位置开始到第 9 个位置,当中间有一个元素删除时,后面的所有元素会向前移动,保证中间没有空余。所以当第 0 个位置的元素删除完毕后,本来在第 1 个位置的元素变成第 0 个元素,但由于 i++ 后 i = 1,下次执行删除操作时会删除最新的第 1 个位置的元素,而第 0 个位置的元素因被跳过而不会被删除,同理当后面的元素被删除时也会有跳过,这就是为什么最终会有 5 个元素的原因。

所以如果只是在遍历 ArrayList 的同时删除指定的元素后并退出循环,这样是没有问题的,因为删除元素后,不会进行其他操作。但是如果删除所有元素,就需要做一些特殊的处理了,具体处理如下:

1
2
3
4
for (int i = 0; i < list.size(); i++) {
list.remove(i);
i--;
}

方法二:使用 Iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
ArrayList<Integer> list = new ArrayList<>();

for (int i = 0; i < 10; i++) {
list.add(i);
}

Iterator<Integer> iterator = list.iterator();

while (iterator.hasNext()) {
if (iterator.next() == 5) {
iterator.remove();
}
}

如果要删除所有元素呢?

1
2
3
4
5
6
7
8
9
10
11
12
ArrayList<Integer> list = new ArrayList<>();

for (int i = 0; i < 10; i++) {
list.add(i);
}

Iterator<Integer> iterator = list.iterator();

while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}

运行结果:

1
执行完毕 list.size() = 0

从运行结果可以知道,程序正常运行而且完全符合预期。为什么使用 Iterator 可以正常遍历并且删除所有元素呢?下面从源码的角度分析:

首先看,list.iterator() 的实现:

1
2
3
public Iterator<E> iterator() {
return new Itr();
}

这个方法创建并返回一个 Itr 类的实例。通过查看源码可以发现 Itr 实现了 Iterator 接口,这不正是我们需要的吗?接下来依次看 iterator.hasNext()、iterator.next() 以及 iterator.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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount; // 因为 Itr 是 ArrayList 的内部类,所以可以直接访问 ArrayList 的 modCount。

Itr() {}

public boolean hasNext() {
// 如果当前 cursor 不指向最后一个元素后面的索引位置,
// 说明当前 cursor 所在位置后面还有没有遍历到的元素,返回 true。
return cursor != size; // 因为 Itr 是 ArrayList 的内部类,所以可以直接访问 ArrayList 的 size。
}

/**
* 首先检查 modCount == expectedModCount 是否成立,
* 如果不成立直接抛出 ConcurrentModificationException;
* 否则按下面的逻辑运行:
* 返回当前 cursor 所在位置的元素,
* lastRet 的值更新为当前 cursor 的值,
* cursor 的值加 1,即指向下次要访问的元素位置。
*/
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

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

/**
* 与直接使用 ArrayList 的 remove() 方法不同的是,
* 该方法会更新 expectedModCount 的值为 modCount。
* 这也正是为什么通过 List.iterator() 遍历的同时进行
* iterator().remove() 操作,不会发送异常的原因。
*/
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();
}
}
}

总结:

使用 List.iterator() 遍历集合的同时进行 iterator().remove() 操作,不会发送异常的原因是:Iterator.remove() 会在执行 ArrayList 的 remove() 后,重新将 expectedModCount 的值更新为 modCount 的值。这样运行 checkForComodification() 方法时,才会正常运行。

方法三:使用增强 for 循环

增强 for 循环是迭代器的简化书写格式,和 iterator 遍历的效果是一样的,也就说增强 for 循环的内部也就是调用 iterator 实现的,只不过获取迭代器由 jvm 完成,不需要我们获取迭代器而已。但是增强 for 循环有些缺点,例如不能在增强 for 循环里动态的删除集合内容(虽然内部也使用了 Iterator 但在删除时因为拿不到 Iterator,所以不能通过 Iterator 删除)、不能获取下标等。

所以这种方式只适用于删除一个指定元素后,立即退出循环的情况。

1
2
3
4
5
6
7
8
9
10
11
12
ArrayList<Integer> list = new ArrayList<>();

for (int i = 0; i < 10; i++) {
list.add(i);
}

for (Integer integer : list) {
if (integer == 5) {
list.remove(integer);
break;
}
}

总结

经过上面对异常发生的原因以及 3 种解决办法的分析,可以得出以下结论:

只要是在遍历时直接通过调用 ArrayList.remove() 移除元素都是不安全的,普通的 for 循环和增强 for 循环适用于删除一个元素后就退出循环体;而 forEach() 循环是坚决不可以执行删除操作的(因为它是程序员无法控制退出循环的),否则会抛出异常。最安全的方法是使用 Iterator 进行遍历的同时,并且必须使用 Iterator 提供的 remove() 方法进行删除操作。

扩展

上面分析是 remove() 操作,那 add() 操作呢?

按照思路应该也是使用 Iterator 的 add 之类的方法,但是当我们写代码的时候会发现,唉?唉?唉?唉?我 iterator 点,iterator 点…… 为什么没有点出来 add 之类的方法?

这是因为 Iterator 接口只提供了 remove() 方法,F***K,这可让我如何是好?别急,List 接口还提供了一个 listIterator() 方法,这个方法返回的对象是可以进行 add 操作的,我们来看一看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ArrayList<Integer> list = new ArrayList<>();

// 初始化 list 中的值为 0,2,4,6,8
for (int i = 0; i < 10; i += 2) {
list.add(i);
}

ListIterator<Integer> iterator = list.listIterator();

// 在每个元素后面追加一个比自己大 1 的元素
while (iterator.hasNext()) {
Integer next = iterator.next();
iterator.add(next + 1);
}

for (Integer integer : list) {
System.out.print(integer);
}

运行结果:

1
0123456789

通过查看源码,可以发现 list.listIterator() 方法如下:

1
2
3
public ListIterator<E> listIterator() {
return new ListItr(0);
}

这个方法返回一个 ListItr 类的实例,并且这个类实现了 ListIterator 接口。

ListIterator 是一个在继承了 Iterator 接口的同时又额外提供了 add() 、set() 等方法的接口,如下图。

ListIterator

再看一下 ListItr 这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private class ListItr extends Itr implements ListIterator<E> {
......
/**
* 同 Itr 的 remove 方法一样,先在集合中添加元素,
* 最后将 modCount 的值赋给 expectedModCount
*/
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}

ListItr 不但实现了 ListIterator 接口,而且继承了 Itr 类(上面已经分析过),从源码中可以发现 ListItr 内没有实现 remove() 方法,显然这是直接复用它的父类 Itr 的 remove() 方法。

所以,如果在遍历的同时只是进行 remove 操作,既可以使用 iterator 也可以使用 listIterator;而要进行 add 操作,就必须使用 listIterator 了。

LinkedList 和 ArrayList 略有不同,虽然它的 iterator() 和 listIterator() 方法返回的接口类型不同,但是仔细分析源码会发现其实内部都是 ListItr 对象。

# 集合 # ConcurrentModificationException # ArrayList # LinkedList
EnvironmentSwitcher
  • 文章目录
  • 站点概览
CodeXiaoMai

CodeXiaoMai

CodeXiaoMai的博客
12 日志
6 分类
19 标签
GitHub E-Mail
  1. 1. 需求与实现
  2. 2. 原因排查
  3. 3. 解决办法
    1. 3.1. 方法一:使用普通的 for 循环
    2. 3.2. 方法二:使用 Iterator
    3. 3.3. 方法三:使用增强 for 循环
  4. 4. 总结
  5. 5. 扩展
© 2019 CodeXiaoMai
|