1. fail-fast简介
fail-fast
是Java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就有可能会发生fail-fast
事件。
例如:当前有一个线程A
在对一个集合(如HashMap
和ArrayList
)进行遍历从操作,在这个过程中,有另外一个线程B
对该集合的结构进行了修改,而这就可能会抛出ConcurrentModificationException
异常,产生fail-fast
事件。
2. fail-fast示例
View Codeimport java.util.*;
import java.util.concurrent.*;
/*
* 功能: java集合中fast-fail的测试程序。
*
* fast-fail事件产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;则会抛出ConcurrentModificationException异常。
* (1)使用ArrayList时,会产生fast-fail事件,抛出ConcurrentModificationException异常
* (2)使用时CopyOnWriteArrayList,不会产生fast-fail事件
*/
public class FastFailTest {
// 两个线程同时对 list 集合进行操作
private static List<String> list = new ArrayList<String>();
public static void main(String[] args) {
// 同时启动两个线程对list进行操作!
new ThreadOne().start();
new ThreadTwo().start();
}
private static void printAll() {
System.out.println("");
String value = null;
Iterator iter = list.iterator();
while(iter.hasNext()) {
value = (String)iter.next();
System.out.print(value+", ");
}
}
/**
* 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list
*/
private static class ThreadOne extends Thread {
public void run() {
int i = 0;
while (i<6) {
list.add(String.valueOf(i));
printAll();
i++;
}
}
}
/**
* 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list
*/
private static class ThreadTwo extends Thread {
public void run() {
int i = 10;
while (i<16) {
list.add(String.valueOf(i));
printAll();
i++;
}
}
}
}
运行结果:程序抛出java.util.ConcurrentModificationException
异常。
结果分析:
- 主程序同时启动两个线程对
list
进行操作 线程A
向list中添加0~5,每添加一个数,则打印整个list
;线程B
向list
中添加10~15,每添加一个数,则同样打印整个list
;- 当其中一个线程在遍历打印
list
的同时,另外一个线程向list
中添加了一个数(既改变了集合的结构),就是就会抛出java.util.ConcurrentModificationException
异常。
3. fail-fast解决办法
fail-fast
机制,是一种错误检测机制,JDK并不保证fail-fast机制一定会发生。
若在多线程环境下使用fail-fast
机制的集合,建议使用java.util.concurrent
包下的类去取代java.util
包下的类。
例如在上面例子中就可以将ArrayList
替换成CopyOnWriteArrayList
:
将:
private static List<String> list = new ArrayList<String>();
替换成:
private static List<String> list = new CopyOnWriteArrayList<String>();
CopyOnWriteArrayList
所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。
该类产生的开销比较大,但是在两种情况下,它非常适合使用。
- 在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
- 当遍历操作的数量大大超过可变操作的数量时。
4. fail-fast原理
产生fail-fast
事件,是通过抛出ConcurrentModificationException
异常来触发的,而要了解fail-fast
机制,我们就需要了解ArrayList
是如何抛出ConcurrentModificationException
异常的。
从前面的代码中,我们了解到ArrayList
是在遍历的过程中,抛出该异常,既在操作Iterator
时抛出。
- ArrayList 的 Iterator 是在父类 AbstractList.java 中实现的,通过源码发现,
Iterator
的next()
和remove()
方法的开始,都会执行一个checkForComodification()
方法。 - 如果
checkForComodification()
方法中,modCount
不等于expectedModCount
,则抛出ConcurrentModificationException
异常。 - 现在我们知道,要了解什么时候会抛出异常,就需要了解,在什么时候
modCount
和expectedModCount
变得不相等了。 - 在创建 Iterator对象时,
modCount
被赋值给expectedModCount
,所以modCount
不等于expectedModCount
只可能是modCount
被修改导致两个变量的不等,所以现在问题变为,modCount
在何时被修改。 - 通过
ArrayList
的源码我们发现,在add()
、remove()
、clear()
方法中,只要涉及到修改集合中的元素个数的时候,都会改变modCount
的值。
4.1 fail-fast的产生步骤
通过以上的步骤找到fail-fast
产生的原因,下面就来看看fail-fast
到底是如何发生的:
- 新建了一个
ArrayList
对象 ->list
,并向其中添加若干个元素; - 新建一个
线程A
,通过Iterator
对list
进行循环遍历 - 新建一个
线程B
,其从list
中删除一个节点X
- 就是就可能会发生我们所说的
fail-fast
事件了:- 某一个时刻,
线程A
创建了Iterator
对象,此时节点X
任然在list
中,在创建Iterator
对象时,modCount == expectedModCount
(假设它们的值为N) - 在
线程A
对list
进行遍历的某一个时刻,线程B
执行了,从list
中删除了节点X
。具体到源码中就是,线程B
执行了remove()
方法,在其中执行了modCount++
,导致modCount
变为了N+1。 - 之后
线程A
继续遍历的时候,执行next()
方法,调用我们上面说到的checkForComodification()
比较modCount
和expectedModCount
的值,此时modCount == N+1
,expectedModCount == N
,两者不等,就抛出ConcurrentModificationException
异常,即产生fail-fast
事件。
- 某一个时刻,
5. 解决fail-fast的原理
- 和 ArrayList 继承于
AbstractList
不同,CopyOnWriteArrayList
没有继承于AbstractList
,它仅仅只是实现了List
接口。 - ArrayList的
iterator()
函数返回的Iterator是在AbstractList中实现的;而CopyOnWriteArrayList
是自己实现Iterator
。 - 同时,新建COWIterator时,它会将集合中的元素保存到一个新的拷贝数组中。这样,当原始集合的数据改变,拷贝数据中的值也不会变化。
- ArrayList 的 Iterator 实现类中调用
next()
时,会调用checkForComodification()
比较expectedModCount
和modCount
的大小;但是,CopyOnWriteArrayList
的 Iterator 实现类中,没有所谓的checkForComodification()
,更不会抛出ConcurrentModificationException
异常!