【Java】【重要特性】详解fail-fast机制
一、什么是fail-fast机制
Fail-fast是快速失败机制,是java集合(Collection)中的一种错误检测机制,主要用于当迭代集合的过程中,该集合在结构上发生改变的时候,就有可能发生fail-fast机制,即抛出ConcurrentModificationException异常。Fail-fast机制并不能保证在不同步的修改下一定会抛出异常,它只是尽最大的努力抛出,所以这种机制一般只是用于检测bug。
二、fail-fast出现的场景
在我们常见的java集合中都有可能出现fail-fast机制,例如ArrayList,HashMap等线程不安全的集合,还有在多线程或单线程环境下都有可能出现fail-fast机制。
总的来说,可能会出现fail-fast机制的前提是一般在集合拿到迭代器迭代的过程中,而该集合元素又发生了变化,如迭代过程中集合的元素被删除修改或新增元素等,都会触发fail-fast机制,程序会尽可能快速抛出ConcurrentModificationException异常。
三、fail-fast实现原理
想要知道fail-fast的原理,那必须弄清楚它是怎么检测到集合结构发生变化并抛出ConcurrentModificationException的。首先先弄清楚一个问题:
怎么触发fail-fast机制?
我们都知道对于集合类、map类等容器,我们可以通过迭代器来遍历;
而迭代器是什么?它就是Iterator,其实就是一个接口,具体的实现是由集合类的内部类去实现Iterator接口并实现其相关方法的。
以ArrayList集合为例,在ArrayList类内部实现Iterator接口是内部类Itr:
//ArrayList调用iterator()方法的时候返回的是Itr的一个新实例
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
//cursor变量主要用于集合在遍历过程中执行即将遍历元素的索引
int cursor; // index of next element to return
//lastRet变量记录上一个遍历的元素的索引,不存在时为-1
int lastRet = -1; // index of last element returned; -1 if no such
//该变量主要用于判断fail-fast机制了,用于记录集合操作过程中被修改的次数,初始值为AbstractList抽象类的modCount值,默认为0.
int expectedModCount = modCount;
//1.用来判断是否有下一个元素,通过当前元素索引cursor是否等于size值
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
//2.next方法遍历下一个元素
public E next() {
//2.1检查该集合元素是否发生改变,发生变化则触发fail-fast机制
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
//2.2获取遍历的集合elementData[i]元素,游标cursor+1,返回
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
//3.删除遍历的第i个集合元素,可以避免直接调用集合类的remove方法而触发fail-fast机制
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//3.1调用集合类里面的remove方法删除,但删除后会重写刷新游标cursor和记录lastRet值,相当于当前正在遍历的集合元素没有发生结构变化。
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
//2.1检查在迭代遍历时集合元素是否被修改过了
final void checkForComodification() {
//检查modCount值(相当于是最新的修改值,位于父类AbstractList的成员变量,实时更新变化)是否等于expectedModCount值(为modCount值的一份拷贝,在开始迭代时获取),如果不相等则说明集合发生改变了,抛出ConcurrentModificationException异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
四、如何避免fail-fast机制
知道了fail-fast机制的实现原理,那么我们就要去避免触发这种机制。
- 如果想要修改删除正在迭代的集合元素,必须使用迭代器里面提供的remove方法,而不是直接调用集合中的remove方法。
- 利用java.util.concurent包中的类来代替我们一般常用的集合类,如利用CopyOnWriteArrayList类代替ArrayList,使用ConcurrentHashMap类代替HashMap等,
CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
对于HashMap,可以使用ConcurrentHashMap,ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。
- 点赞
- 收藏
- 关注作者
评论(0)