Fail-Fast 问题
如果你想在使用 Iterator(迭代器)进行遍历的过程中,移除 List 中的某个元素,只能调用 iterator.remove 方法,而不能调用 list.remove () 方法,否则一定会抛出并发修改异常(java.util.ConcurrentModificationException)。
注意,当这个异常被抛出时,并不意味着这个 list 一定正在被多个线程同时使用,而在同一个线程中既一边遍历 list,一边调用 list.remove () 方法,也会抛出 ConcurrentModificationException。
一个错误的实现
public class Test1 {
private static List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int i = iterator.next();
// remove a specific element
list.remove(5);
System.out.println("ThreadOne 遍历:" + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果
ThreadOne 遍历:0
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.concretepage.lang.Test1.main(Test1.java:15)
又一个错误的实现
类似地,如果我们在另一个线程中调用 list.remove (),情况也是一样的(同样会抛出并发修改异常 java.util.ConcurrentModificationException)。
public class Test2 {
private static List<Integer> list = new ArrayList<>();
private static class threadOne extends Thread {
public void run() {
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int i = iterator.next();
System.out.println("ThreadOne 遍历:" + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static class threadTwo extends Thread {
public void run() {
int i = 0;
while (i < 6) {
System.out.println("ThreadTwo run:" + i);
if (i == 3) {
list.remove(i);
break;
}
i++;
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
list.add(i);
}
new threadOne().start();
new threadTwo().start();
}
}
执行结果
ThreadOne 遍历:0
ThreadTwo run:0
ThreadTwo run:1
ThreadTwo run:2
ThreadTwo run:3
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.concretepage.lang.TestString$threadOne.run(Test3.java:12)
一个正确的实现
public class Test3 {
private static List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int i = iterator.next();
if (i == 3)
// remove a specific element
iterator.remove();
}
System.out.println(list);
}
}
执行结果
[0, 1, 2, 4, 5, 6, 7, 8, 9]
Fail-Fast 产生原因
Fail-Fast 产生的原因就在于程序在对 collection 进行迭代时,同时对这个 collection 进行了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 Fail-Fast。
我们来看看 ArrayList 中迭代器的源代码:
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount;
public boolean hasNext() {
return (this.cursor != ArrayList.this.size);
}
public E next() {
checkForComodification();
/** 省略此处代码 */
}
public void remove() {
if (this.lastRet < 0)
throw new IllegalStateException();
checkForComodification();
/** 省略此处代码 */
}
final void checkForComodification() {
if (ArrayList.this.modCount == this.expectedModCount)
return;
throw new ConcurrentModificationException();
}
}
从上面的源代码我们可以看出,在调用迭代器实例对象的 next ()、remove () 方法时,都会在内部调用 checkForComodification () 方法,该方法主要就是检查是否满足 modCount == expectedModCount 。
若不满足,则抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。
而 ArrayList 的 iterator () 实现如下,本质返回一个新实施化的 Itr 类。
public Iterator<E> iterator() {
return new Itr();
}
而 expectedModCount 是在 Itr 中定义的:
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
...
expectedModCount 的值在 Itr 对象的整个生命周期中,是没被修改的,所以会变的就是 modCount。modCount 是在 AbstractList 中定义的,为全局变量:
protected transient int modCount = 0;
那么 modCount 什么时候,因为什么原因而发生改变呢?
事实上,只要调用了 ArrayList 实例对象的 add ()、remove () 或 clear () 方法中的任何一个(即只要改变 ArrayList 中元素的个数)都会导致 modCount 的改变。
此后,expectedModCount 与 modCount 的值就会不相等,从而引发 fail-fast 机制。
Fail-Fast 解决办法
可以使用 CopyOnWriteArrayList 来替换 ArrayList。
CopyOnWriteArrayList 为何物?
CopyOnWriteArrayList 是 ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。
因此,该类产生的开销比较大,但是在两种情况下,它非常适合使用:
- 在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
- 当遍历操作的数量大大超过可变操作的数量时。
遇到这两种情况使用 CopyOnWriteArrayList 来替代 ArrayList 再适合不过了。
那么,为什么 CopyOnWriterArrayList 可以替代 ArrayList 呢?
- CopyOnWriterArrayList 的无论是从数据结构、定义都和 ArrayList 一样。它和 ArrayList 一样,同样是实现 List 接口,底层使用数组实现。在方法上也包含 add、remove、clear、iterator 等方法。
- CopyOnWriterArrayList 根本就不会产生 ConcurrentModificationException 异常,也就是它使用迭代器完全不会产生 fail-fast 机制。
Reference
- Java 提高篇(三四)—–fail-fast 机制 - https://www.cnblogs.com/chenssy/p/3870107.html
- 为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作 - https://www.hollischuang.com/archives/3304