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