1、Fail-Fast(快速失败)简介
fail-fast 机制是java集合(Collection)中的一种错误机制,当单线程或者多个线程在集合遍历过程中对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
单线程例如:当通过iterator去遍历某集合时,在遍历过程中修改集合中的元素添加、删除就会导致Fail-Fast
代码如:
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
list.remove(iterator.next()); //此步调用会导致Fail-Fast
}
}
多线程例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
代码如:
public class Main {
private static ArrayList arrayList = new ArrayList();
private static void Print(){
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
arrayList.add(i);
Print();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 5; i < 10; i++) {
arrayList.add(i);
Print();
}
}
});
thread1.start();
thread2.start();
}
2、Fail-Fast的解决办法
fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生
想解决Fail-Fast只需要使用java.util.concurrent下面的包去替换java.util下面的包
将代码
private static ArrayList arrayList = new ArrayList();
替换为
private static CopyOnWriteArrayList arrayList = new CopyOnWriteArrayList();
3、Fail-Fast的原理
在抛出异常的时候我们可以发现抛出的异常为
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
那么我们就顺着去寻找ArrayList.java:901
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
发现了这个函数,根据函数名我们可以简单的理解为
如果修改的次数和我们期望的修改次数不同的话,就抛出ConcurrentModificationException这个异常
那么expectedModCount一开始的值是多少呢?
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;
...
}
在ArrayList的内部类Itr中我们发现,expectedModCount的值在迭代开始时应该和modCount的值是相等的
好了,我们只要寻找出来为什么不同就应该能理解Fail-Fast的原理了
等等,刚刚的异常里面还有一句我们没看
at java.util.ArrayList$Itr.next(ArrayList.java:851)
我们翻到ArrayList.java:851
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];
}
显示的是next()函数,在next函数中首先便调用了checkForComodification()这个方法
到这里我们了解到了,Fail-Fast异常果然和迭代器有关,那么我们现在就需要了解到
modCount != expectedModCount是怎么产生的
首先,我们先打开ArrayList源码中ensureExplicitCapacity(int minCapacity)方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
我们发现,只有执行了ensureExplicitCapacity(int minCapacity)方法,就会导致modCount++
那么这个方法为什么会执行呢
我们打开add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
我们发现在调用add方法时,首先就会调用ensureCapacityInternal方法,来确保数组的容量足够加入新的元素
那么在这个时候就会改变modCount的值,使得modCount的值和所期望的值不一样从而导出异常
那么我们再看看remove方法
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
果然同样的,出现了modCount++这一语句
4、Fail-Fast的总结
这时我们就能理解了Fail-Fast的原理了
①在集合进行增删或者其他修改集合元素的方法时,代码中会保留修改次数既modCount的值
②在调用迭代器时,会将这个modCount的值赋予给迭代器中的expectedModCount
③在调用迭代器的过程中,每执行一次next()都会检测expectedModCount和modCount都值是否相等
④在迭代过程中,如果调用迭代器的remove()方法,不会造成modCount值的改变
⑤如果执行了集合的remove()方法,则会造成modCount值的改变,于预期的值不服而抛出异常
5、为什么 CopyOnWriteArrayList能避免Fail-Fast错误
我们打开 CopyOnWriteArrayList.java
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
我们发现在执行添加操作的时候,会首先把原集合的数组的值复制到新的数组里面去,在新数组里面添加元素
因为新数组没有执行迭代器,当然不会抛出Fail-Fast异常