➤ 问题 | 并发修改异常分析

背景

如下代码报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> it = list.iterator();
// 如果 list 里面有 "b",则添加 "d"
while (it.hasNext()) {
String s = it.next(); // ConcurrentModificationException
if (s.equals("b")) {
list.add("d");
}
}
System.out.println(list);
}
}

可以看到抛出的异常名为: ConcurrentModificationException

为什么集合删除元素不能用 for-each/Iterator?

源码分析

部分源码

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
// List 源码
public interface List<E> {
Iterator<E> iterator();
boolean add(E e);
}
// ArrayList 源码
public class ArrayList<E> extends AbstractList<E> implements List<E> {
public Iterator<E> iterator() { // ------ 1
return new Itr(); // ------ 2
}
public boolean add(E e) {
modCount++; // ------ 9
add(e, elementData, size);
return true;
}

private class Itr implements Iterator<E> { // ------ 3
/*
modcount:实际修改集合的次数
expectedmodcount:预期修改集合的次数
*/
int expectedModCount = modCount; // ------ 7

public E next() { // ------ 4
checkForComodification(); // ------ 5
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() { // ------ 6
// 修改集合的次数 != 预期修改集合的次数,会抛出异常
if (modCount != expectedModCount) // ------ 10
throw new ConcurrentModificationException();
}
}
}
// AbstractList 源码
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
protected transient int modCount = 0; // ------ 8
}

分析

我们报错的地方在 String s = it.next(); 这里,所以我们要看看这里面发生了什么,也就是程序怎么走的:

  1. 首先使用了 iterator() 方法
  2. 这个 iterator() 方法返回一个new Itr()
  3. 进入到 Itr 这个类里面
  4. Itr 中有一个 next() 方法,而这个也就是我程序里调用的 next()
  5. 这个 next() 方法里面有一个 checkForComodification() 方法
  6. 进入到 checkForComodification() 这个方法里面,发现了一个条件语句,里面抛出 ConcurrentModificationException 异常,这里应该就是 “病根” 了!条件语句是 if (modCount != expectedModCount)
  7. 找到 expectedModCount 这个变量,这个变量最开始是和 modCount 相等的,所以是什么原因导致了这俩变量不相等了,继续找 modCount
  8. Alt-Enter 进入 modCount ,我们在 ArrayList 的父类 AbstractList 里面找到了该变量
  9. 当执行 ArrayList 里面的 add() 方法时,也就是 list.add("d"); 这一操作,使得 modCount++
  10. 最终导致了 modCount != expectedModCount

解决

不要去使用 Iterator,可以使用 for ,或是去使用 ListIterator

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
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
// 如果 list 里面有 "b",则添加 "d"
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
if (s.equals("b")) {
list.add("d");
}
}
System.out.println(list); // [a, b, c, d]
}
}

public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
ListIterator<String> listIt = list.listIterator();
// 如果 list 里面有 "b",则添加 "d"
while (listIt.hasNext()) {
String s = listIt.next();
if (s.equals("b")) {
listIt.add("d"); // 注意这里不是 list,而是 listIt
}
}
System.out.println(list); // [a, b, c, d]
}
}

为什么 get() 就可以呢?看看 ArrayList 里面的 get() 的源码:

1
2
3
4
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}

可以看到它并没有设置判断,可是为什么 ListIterator 就可以呢?

同样按照上面的源码分析过程看看它的 ListItr 类里面的 add() 方法,可以知道 ListItr 类的 add() 方法内,会重新将 “预期修改集合的次数” 赋值为 “修改集合的次数”,所以不会抛出并发修改异常。

总结

  • 每次使用 add() 方法时,“修改集合的次数” 就会 +1
  • “预期修改集合的次数” 初值等于 “修改集合的次数”
  • 在使用 Iterator 进行遍历时,每次执行 next() 方法,都会判断一下 “修改集合的次数” 是否等于 “预期修改集合的次数”,如果不等,则抛出并发修改异常
  • 也就是说,在遍历期间执行 add() 会导致 “修改集合的次数” != “预期修改集合的次数”,进而导致抛出并发修改异常,即,ConcurrentModificationException 异常
  • 可以使用 ArrayList 里面的 get() 方法解决这样的问题
  • 也可以使用 ListItr 类的 add() 方法来解决
  • 为避免混淆,再此说明下: Iterator 接口里面没有 add() 方法,在添加时使用的是 ArrayList 里面的 add() 方法;而 ListIterator 接口里面是有 add() 方法的,所以添加时使用的是 ListItr 类的 add() 方法