1.概念
fail-fast 机制是java集合(Collection)中的一种错误机制。
当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
2.示例
- 假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
如下代码:
private static List<Integer> list = new ArrayList<>();
/**
* 线程One迭代list
*/
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();
}
}
}
}
/**
* 当i == 3时,修改list
*/
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);
}
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
ThreadTwo run:4
ThreadTwo run:5
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayListItr.next(ArrayList.java:851)
at com.design.prompt.FailFastDemo$threadOne.run(FailFastDemo.java:17)
错误发生在: int i = iterator.next(); 此行代码
分析:根据错误发生位置可以初步了解到fail-fast产生的原因就在于程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast。
注意:该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出该异常。
诚然:迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常,所以因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。下面我将以ArrayList为例进一步分析fail-fast产生的原因。
因此,查看ArrayList中迭代器的源代码
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
- 可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。所以要弄清楚为什么会产生fail-fast机制我们就必须要用弄明白为什么modCount != expectedModCount ,他们的值在什么时候发生改变的?
- expectedModCount 是在Itr中定义的:int expectedModCount = modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是继承 AbstractList 中定义的,为全局变量:protected transient int modCount = 0;
- 通过查看ArrayList源码可以看出ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。
package com.design.prompt;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class FailFastDemo {
public static void main(String[] args) {
//这会报不支持的操作异常,UnsupportedOperationException
//因为,Arrays.assList的底层实现是使用数组实现的,而数组不支持remove操作。
String string = "a b c d e";
List<String> stringList = Arrays.asList(string.split(" "));
Iterator<String> iterator = stringList.iterator();
while (iterator.hasNext()) {
if(iterator.next().equals("c")) {
stringList.remove("c");
}
}
}
}
运行结果:
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.remove(AbstractList.java:161)
at java.util.AbstractList$Itr.remove(AbstractList.java:374)
at java.util.AbstractCollection.remove(AbstractCollection.java:293)
at com.design.prompt.FailFastDemo.main(FailFastDemo.java:51)
报错的行数是:stringList.remove("c"); 这句代码。
分析:原因在于List<String> stringList = Arrays.asList(string.split(" ")); 这句话。Arrays.asList返回的List是固定大小的List,也就是说不可以对其进行add、remove操作。
注:此处的ArrayList来自Array的子类,代码如下
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
/**
* 该ArrayList类不是java.util包中的ArrayList,而是Arrays类的内部类ArrayList。
*/
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
/*省略此处代码*/
}
从Array类的内部类ArrayList中的代码可以看出,返回的是固定大小的List。从源代码private final E[] a; 这句看到,内部类ArrayList中的成员变量a,是final的。故不能对其进行add、remove操作。
public static void main(String[] args) {
String string = "a b c d e";
List<String> stringList1 = Arrays.asList(string.split(" "));
//这个会得到 并发修改异常 ConcurrentModificationException
List<String> stringList = new ArrayList<String>(stringList1);
System.out.println(stringList);
Iterator<String> iterator = stringList.iterator();
while (iterator.hasNext()) {
if(iterator.next().equals("c")) {
stringList.remove("c");
}
}
}
运行结果:
[a, b, c, d, e]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayListItr.next(ArrayList.java:851)
at com.design.prompt.FailFastDemo.main(FailFastDemo.java:18)
异常源于:if(iterator.next().equals("c")) { ,这行代码。
分析:
- java.util.ArrayList的内部类Itr的next方法在调用checkForComodification方法时抛的异常。
- 其中的modCount是ArrayList继承于AbstractList的一个属性。
protected transient int modCount = 0;
- transient关键字,说明ArrayList在序列化时,不包含modCound属性。
- modCount用来表示list被结构性修改的次数。所谓结构性修改是指,改变了list的size,或者在迭代过程中产生了不正确的结果。
- 内部类在被实例化时会将modCount赋值给expectedModCount。
内部类的实例化发生在ArrayList的iterator被调用的时候, - Iterator<String> iterator = stringList.iterator();这句话执行时初始化的。看看ArrayList#iterator的实现:
public Iterator<E> iterator() {
return new Itr();
}
从next方法的实现上可以看到,首先就会判断expectedModCount与modCount是否相等,如果不等就会抛ConcurrentModificationException异常。不相等说明其他线程修改了modCount。当然,在这个例子中只有main线程,也出现了这种情况。因为,在迭代到”c”时,stringList.remove("c"); 执行了remove操作,对list造成了结构性修改,改变了list的size,modCount的值会加1。这样当迭代到”d”时,发现expectedModCount与modCount不等了,因此抛异常了。