东东
发布于 2022-10-09 / 21 阅读 / 0 评论 / 0 点赞

fail-fast(快速失败)机制和fail-safe(安全失败)机制

fail-fast和fail-safe的区别:

fail-safe允许在遍历的过程中对容器中的数据进行修改,而fail-fast则不允许。

  • Fail-fast:不允许同时遍历修改,一旦发现遍历的同时其它人来修改的话,会立刻抛出异常。
  • Fail-safe:允许同时遍历修改,但相对牺牲了一致性来保证整个遍历运行完成。

fail-fast

定义:

fail-fast:直接在容器上进行遍历,在遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException异常导致遍历失败。
java.util包下的集合类都是快速失败机制的, 常见的的使用fail-fast方式遍历的容器有HashMap和ArrayList等。

场景:

在多线程和单线程环境下都有可能出现快速失败。

1、单线程环境

1.1、ArrayList

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    for (int i = 0 ; i < 10 ; i++ ) {
        list.add(i + "");
    }
    Iterator<String> iterator = list.iterator();
    int i = 0 ;
    while(iterator.hasNext()) {
        if (i == 3) {
             list.remove(3);
        }
        System.out.println(iterator.next());
        i ++;
    }

2、多线程环境

2.1、ArrayList

原理

查看迭代器源码:


int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
  • cursor是指集合遍历过程中的即将遍历的元素的索引;
  • lastRet是cursor-1,默认为-1,即不存在上一个时,为-1,它主要用于记录刚刚遍历过的元素的索引。
  • expectedModCount这个就是fail-fast判断的关键变量了,它初始值就为ArrayList中的modCount。

modCount是抽象类AbstractList中的变量,默认为0,而ArrayList 继承了AbstractList ,所以也有这个变量,modCount用于记录集合操作过程中作的修改次数,与size还是有区别的,并不一定等于size

查看next()的源码:

@SuppressWarnings("unchecked")
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
         throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
         throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

代码可以看到,方法中首先会进行一次校验:checkForComodification();

final void checkForComodification() {
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
}
  • 首先在遍历的开始,expectedModCount初始值默认等于modCount;
  • expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变
  • 发生改变的就只有modCount,在前面关于ArrayList扩容机制的分析中,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++)
  • 类似的,HashMap中也是相同原理

解决

方法一:使用迭代器中自带的 remove() 方法

该remove方法并不会修改modCount的值,并且不会对后面的遍历造成影响,因为该方法remove不能指定元素,只能remove当前遍历过的那个元素,所以调用该方法并不会发生fail-fast现象。该方法有局限性。

方法二:使用 fail-safe 机制,

使用java并发包(java.util.concurrent)中的CopyOnWriterArrayList类来代替ArrayList,使用 ConcurrentHashMap来代替hashMap。

fail-safe

这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等。

原理:

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:

基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,
即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。


本文引用

https://blog.csdn.net/striner/article/details/86375684
https://blog.csdn.net/qiao1f/article/details/123551552