一、什么是快速失败机制
ArrayList实现了一种称为快速失败(fail-fast)的机制,该机制在并发修改时会抛出ConcurrentModificationException异常。
这种机制的实现原理是:ArrayList在遍历时会记录列表的修改总数(通过modCount字段),如果在遍历过程中列表结构发生变化,那么modCount的值会增大。每次遍历前,迭代器都会检查modCount是否发生改变,如果改变则抛出异常,表示列表并发修改。
这个机制的作用是确保在遍历列时候,列表的结构保持稳定,避免并发修改带来未知的结果。
二、例子
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for(String s : list){
if("C".equals(s)){
list.remove(s);
}
}
System.out.println(list);
因为在遍历过程中调用 list.remove(s) 修改了列表结构,这会导致modCount的值增加,迭代器在下一步遍历前检查到modCount改变,所以抛出异常。
三、底层原理
AbstractList中定义了modCount并初始化为0,modCount是用来记录修改表结构的次数
add操作会将modCount+1
remove方法同样会使modCount++
看到ArrayList中的迭代器的实现,先将expectModCount的值设为modCount的值,在next()方法中最开始会执行一个checkForComodification()方法
这个方法的作用就是检查expectedModCount与modCount是否相等,如果在迭代遍历过程中发生了对list结构的修改操作,modCount的值就不会跟expectedModCount相等,然后抛出异常
二中的例子代码在编译后的结果
List<String> list = new ArrayList();
list.add("A");
list.add("B");
list.add("C");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if ("C".equals(s)) {
list.remove(s);
}
}
System.out.println(list);
可以看到增强for循环其实本质上就是使用迭代器去遍历
List<String> list = new ArrayList();
list.add("A"); // modCount = 1
list.add("B"); // modCount = 2
list.add("C"); // modCount = 3
Iterator var2 = list.iterator(); // expectedModCount = modCount = 3
while(var2.hasNext()) {
String s = (String)var2.next(); // 会检查expectedModCount与modCount是否相等
if ("C".equals(s)) {
list.remove(s); // modCount = 4
}
}
System.out.println(list);
这里有人会问,这里明明已经执行到最后一个元素了,会跳出循环,就不会执行next方法,也就不会抛出异常了。看看hasNext()方法的实现
如果没有删除C元素,此时cursor = size = 3
但是因为remove了一个元素,此时数组大小size变成2,而cursor为3,依旧会进入循环去执行next方法,这次执行就会抛出异常。
四、解决方法
要解决这个问题,我们有几种方式:
- 遍历结束后再修改列表结构。这是最简单的方式。
- 使用迭代器自己的remove()方法。该方法会判断删除元素是否是迭代器当前元素,如果不是则抛出异常,确保安全。
- 使用更高级的并发集合,如CopyOnWriteArrayList。该集合在修改时会返回集合的新副本,确保线程安全。
- 使用ListIterator,它支持在遍历过程中安全添加和删除。
- 调用removeIf()等在遍历时支持删除的方法。
五、快速失败机制的一个小bug
这里使用的是jdk11
List<String> list = new ArrayList();
list.add("A");
list.add("B");
list.add("C");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if ("B".equals(s)) {
list.remove(s);
}
}
System.out.println(list);
if (“B”.equals(s)) 当我把C改成B后执行代码的结果
可以看到神奇的执行成功了,并没有抛出异常。。。
这里算是一个小bug把,ArrayList如果删除的元素刚好是倒数第二个元素时,不会触发快速失败机制
看到编译后的代码执行过程
List<String> list = new ArrayList();
list.add("A"); // modCount = 1
list.add("B"); // modCount = 2
list.add("C"); // modCount = 3
Iterator var2 = list.iterator(); // expectedModCount = modCount = 3
while(var2.hasNext()) {
String s = (String)var2.next(); // 会检查expectedModCount与modCount是否相等
if ("B".equals(s)) {
list.remove(s); // modCount = 4
}
}
System.out.println(list);
在执行完移除B的操作后,此时数组变成了 [A,C],数组size变成了2,又因为在执行完next方法之后会将迭代器的下标cursor+1指向下一个元素,此时cursor也为2,然后在hasNext方法里判断就会为false,直接跳出了循环(没有执行会抛异常的那个next方法),导致这次代码执行是成功的。