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) { |