阅读阿里开发手册发现的ConcurrentModificationException异常

今天阅读阿里巴巴Java开发手册(终极版).pdf,偶然发现一条强制规范:

【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator
方式,如果并发操作,需要对 Iterator 对象加锁。

这引发了我的注意力。

按照他说的做了一遍,果然有这个问题。下面我们就探索一下,到底为什么会出这个问题吧!

想要解锁更多新姿势?请访问https://blog.tengshe789.tech/

实验代码

下面是推荐使用的Iterator方式来remove/add操作

1
2
3
4
5
6
7
8
9
10
public static List<String> test1(List<String> list){
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals(NUM2)) {
iterator.remove();
}
}
return list;
}

不推荐使用的方法:

1
2
3
4
5
6
7
8
public static List<String> test2(List<String> list){
for (String item : list) {
if (NUM2.equals(item)) {
list.remove(item);
}
}
return list;
}

我的main方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static final String NUM1 = "1";
public static final String NUM2 = "2";

public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");

test2(list);

list.forEach(item -> {
System.out.println("结果是 " + item);
});
}

实验结果

ConcurrentModificationException异常
果然,将判断条件设置成NUM2时,就出异常了,可是这是为什么呢?

一探究竟

我们从出错日志中查找答案。

1
2
3
4
5
6
7
8
9
10
11
12
@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];
}

来到foreach中走的next(),第一步需要走checkForComodification()

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

这边又出现一个成员变量modCount,我们顺着来找找看。

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
/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*
* <p>This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
*/
protected transient int transient = 0;

哦,缘来乳此,list是线程不安全的,因此如果在使用迭代器的过程中有其他线程修改了list,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略(见jdk注释)。

modCount它代表该List对象被修改的次数,每对List对象修改一次,modCount都会加Iterator类里有一个成员变量expectedModCount,它的值为创建Iterator对象的时候List的modCount值。用此变量来检验在迭代过程中List对象是否被修改了,如果被修改了则抛出java.util.ConcurrentModificationException异常。在每次调用Iterator对象的next()方法的时候都会调用checkForComodification()方法进行一次检验,checkForComodification()方法中做的工作就是比较expectedModCountmodCount的值是否相等,如果不相等,就认为还有其他对象正在对当前的List进行操作,那个就会抛出ConcurrentModificationException异常。

网上查找的关于Iterator的工作机制。Iterator是工作在一个独立的线程中,并且拥有一个 mutex锁,就是说Iterator在工作的时候,是不允许被迭代的对象被改变的。而List等是动态的,可变对象数量的数据结构,但是Iterator则是单向不可变,只能顺序读取,不能逆序操作的数据结构,当 Iterator指向的原始数据发生变化时,Iterator自己就迷失了方向。

续1s时间

全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧!

谢谢你那么可爱,还一直关注着我~❤😝

-------------本稿が終わる感谢您的阅读-------------