1 fail-fast简介
- fail-fast是java集合(Collection)的一种错误机制
- 当多个线程对同一个集合的内容进行操作时,就可能产生fail-fast事件
2 代码测试
package basicKnowledge.集合框架.arrayList;
import java.util.ArrayList;
import java.util.Iterator;
/**
* @program:summary
* @author:peicc
* @create:2019-07-19 15:23:29
**/
//测试fast-fail机制
public class Test_fast_fail {
static ArrayList<Integer> list=new ArrayList<Integer>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
list.add(i);
printAll();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i=10;i<20;i++){
list.add(i);
printAll();
}
}
}).start();
}
private static void printAll(){
Iterator iterator=list.iterator();
while(iterator.hasNext()){
System.out.print(iterator.next()+",");
}
}
}
运行结果
"C:\Program Files\Java\jdk1.8.0_191\bin\java.exe"
0,0,10,0,10,11,0,10,11,12,0,10,11,12,13,0,10,11,12,13,14,0,10,11,12,13,14,15,0,10,11,12,13,14,15,16,0,10,11,12,13,14,15,16,17,0,10,11,12,13,14,15,16,17,18,0,10,11,12,13,14,15,16,17,18,19,Exception in thread "Thread-0" Disconnected from the target VM, address: '127.0.0.1:56115', transport: 'socket'
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at basicKnowledge.集合框架.arrayList.Test_fast_fail.printAll(Test_fast_fail.java:38)
at basicKnowledge.集合框架.arrayList.Test_fast_fail.access$000(Test_fast_fail.java:12)
at basicKnowledge.集合框架.arrayList.Test_fast_fail$1.run(Test_fast_fail.java:21)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
3 fast-fail原理
- fast-fail是在操作Iterator时产生的
- Iterator在执行next()以及remove()时,都会执行checkForComodifacation
- 在checkForComodifacation()方法中,当modCount !=expectedModCount时,抛出ConcurrentModificationException异常,产生fail-fast事件
- 什么时候modCount !=expectedModCount?
- expectedModCount 在创建Itr对象时,被赋值为 modCount。所以expectedModCount 不可能修改
- 因此,问题出现在modCount上。接下来,需要考虑的问题是,modCount什么时候会被修改?
- 通过查看源码,可以知道,add()、remove(),还是clear(),只要涉及到修改集合中的元素个数时,都会改变modCount的值
4 解决办法
- 使用“java.util.concurrent包下的类”去取代“java.util包下的类”
5 解决实现原理
以和ArrayList对应的CopyOnWriteArrayList进行说明
首先看下Iterator的源码
//iterator()函数
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
- 通过构造函数将数组引用传递给迭代器
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code remove}
* is not supported by this iterator.
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code set}
* is not supported by this iterator.
*/
public void set(E e) {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code add}
* is not supported by this iterator.
*/
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] elements = snapshot;
final int size = elements.length;
for (int i = cursor; i < size; i++) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
cursor = size;
}
}
- 构造方法中,将引用复制一份给快照
- 数组引用为什么要复制一份给快照?
- 因为之后原数组引用会指向新数组,如果不保存原数组引用的话,此时迭代器操作的将是新数组
再来看下 CopyOnWriteArrayList相关操作的源码(add、remove等)
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}
- 在进行add() 等会改变原数组的操作中,首先copy一份原数组,然后在copy的数组上进行操作,最后将copy的引用赋值给原数组
- 而迭代器操作的数组是在调用Iterator()方法时传递进去原数组(下图中上面的数组),但是可能操作的并不是最新的数据
- 示意图如下
参考文献
1.JDK1.8 源码
2.Java 集合系列04之 fail-fast总结(通过ArrayList来说明fail-fast的原理、解决办法)