Fail-fast机制
它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
我们通过ArrayList源代码来分析,先看下迭代器的源码
1 |
|
从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。所以我们要确定什么时候modCount != expectedModCount,他们的值在什么时候发生改变的。
我们看源码可以知道expectedModCount 是在Itr中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount,modCount是在 AbstractList 中定义的,为全局变量。
1 | protected transient int modCount = 0; |
再看它什么时候会改变,
1 | public boolean add(E paramE) { |
从上面的源代码我们可以看出,ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。知道产生fail-fast产生的根本原因了,我们可以有如下场景:有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount = N ,而modCount = N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。
fail-fast解决办法
- 在遍历过程中所有涉及到改变
modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。 - 使用
CopyOnWriteArrayList来替换ArrayList
CopyOnWriteArrayList
它是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。
- 在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
- 当遍历操作的数量大大超过可变操作的数量时。
CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。
CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。以add方法为例,它是在于copy原来的array,再在copy数组上进行add操作,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。
Foreach
我们知道在JAVA中,遍历集合和数组一般有以下三种形式:第一种是普通的for循环遍历、第二种是使用迭代器进行遍历,第三种我们一般称之为增强for循环(for each)。
那么Foreach底层是怎么做的,使得它能够进行循环。
我们对以下代码进行反编译:
1 | for (Integer i : list) { |
反编译后:
1 | Integer i; |
通过反编译,我们看到,其实JAVA中的增强for循环底层是通过迭代器模式来实现的。那么必然就会有Fail-fast机制,在使用迭代器遍历元素的时候,在对集合进行删除的时候一定要注意,使用不当有可能发生ConcurrentModificationException。
Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用Iterator 本身的方法 remove() 来删除对象,Iterator.remove()方法会在删除当前迭代对象的同时维护索引的一致性。
1 | for (Student stu : students) { |