快速失败(fail-fast)与安全失败(fail-safe)

fail-fast与fail-safe

在Collection集合的各个类中,有线程安全和线程不安全这2大类的版本。java

对于线程不安全的类,并发状况下可能会出现fail-fast状况;而线程安全的类,可能出现fail-safe的状况。数组

1、并发修改

当一个或多个线程正在遍历一个集合Collection的时候(Iterator遍历),而此时另外一个线程修改了这个集合的内容(如添加,删除或者修改)。这就是并发修改的状况。安全

2、fail-fast快速失败

fail-fast机制:当遍历一个集合对象时,若是集合对象的结构被修改了,就会抛出ConcurrentModificationExcetion异常。多线程

有2种状况会抛出该异常:并发

  1. 在单线程的状况下,若是使用Iterator对象遍历集合对象的过程当中,修改了集合对象的结构。以下:spa

    // 1.iterator迭代,抛出ConcurrentModificationException异常
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
      String s = iterator.next();
      System.out.println(s);
      // 修改集合结构
      if ("s2".equals(s)) {
        list.remove(s);
      }
    }
    
    // 2.foreach迭代,抛出ConcurrentModificationException异常
    for (String s : list) {
      System.out.println(s);
      // 修改集合结构
      if ("s2".equals(s)) {
        list.remove(s);
      }
    }

    要想避免抛出异常,应该使用Iterator对象的remove()方法。线程

    // 3.iterator迭代,使用iterator.remove()移除元素不会抛出异常
    Iterator<String> iterator2 = list.iterator();
    while (iterator2.hasNext()) {
      String s = iterator2.next();
      System.out.println(s);
      // 修改集合结构
      if ("s2".equals(s)) {
      iterator2.remove();
      }
    }
  2. 在多线程环境下,若是对集合对象进行并发修改,那么就会抛出ConcurrentModificationException异常。

注意,迭代器的快速失败行为没法获得保证,由于通常来讲,不可能对是否出现不一样步并发修改作出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。所以,为提升这类迭代器的正确性而编写一个依赖于此异常的程序是错误的作法,迭代器的快速失败行为应该仅用于检测 bug。code

以ArrayList为例,讲解一下fail-fast的机制

一、单线程下,使用iterator迭代时的状况

ArrayList继承自AbstractList类,AbstractList内部有一个字段modCount,表明修改的次数。对象

fail-1

ArrayList类的add、remove操做都会使得modCount自增。blog

fail-2

fail-3

当使用ArrayList.iterator()返回一个迭代器对象时。迭代器对象有一个属性expectedModCount,它被赋值为该方法调用时modCount的值。这意味着,这个值是modCount在这个时间点的快照值,expectedModCount值在iterator对象内部不会再发送变化!

fail-4

这时候咱们就能明白了,在获得迭代器以后,若是咱们使用ArrayList的add、remove等方法,会使得modCount的值自增(发生了变化),而iterator内部的expectedModCount值却仍是以前的快照值。咱们再来看iterator的方法实现:能够看到,在调用next方法时,第一步就是检查modCount值和迭代器内部的expectedModCount值是否相等!显然,这是不等的,因此在调用next方法的时候,就抛出了ConcurrentModificationException异常。

fail-5

fail-6

为何说迭代器的fail-fast机制是尽最大努力地抛出ConcurrentModificationException异常呢?

缘由就是上面咱们看到的,只有在迭代过程当中修改了元素的结构,当再调用next()方法时才会抛出该异常。也就是说,若是迭代过程当中发生了修改,但以后没有调用next()迭代,该异常就不会抛出了!(该异常的机制是告诉你,当前迭代器要进行操做是有问题的,由于集合对象如今的状态发生了改变!)

那为何iterator.remove()方法可行呢?

下图中,能够看到,remove方法没有进行modCount值的检查,而且手动把expectedModCount值修改为了modCount值,这又保证了下一次迭代的正确。

fail-7

二、多线程下的状况

固然,若是多线程下使用迭代器也会抛出ConcurrentModificationException异常。而若是不进行迭代遍历,而是并发修改集合类,则可能会出现其余的异常如数组越界异常。

3、fail-safe安全失败

Fail-Safe 迭代的出现,是为了解决fail-fast抛出异常处理不方便的状况。fail-safe是针对线程安全的集合类。

上面的fail-fast发生时,程序会抛出异常,而fail-safe是一个概念,并发容器的并发修改不会抛出异常,这和其实现有关。并发容器的iterate方法返回的iterator对象,内部都是保存了该集合对象的一个快照副本,而且没有modCount等数值作检查。以下图,这也形成了并发容器的iterator读取的数据是某个时间点的快照版本。你能够并发读取,不会抛出异常,可是不保证你遍历读取的值和当前集合对象的状态是一致的!这就是安全失败的含义。

fail-8

因此Fail-Safe 迭代的缺点是:首先是iterator不能保证返回集合更新后的数据,由于其工做在集合克隆上,而非集合自己。其次,建立集合拷贝须要相应的开销,包括时间和内存。

在java.util.concurrent 包中集合的迭代器,如 ConcurrentHashMap, CopyOnWriteArrayList等默认为都是Fail-Safe。

// 1.foreach迭代,fail-safe,不会抛出异常
for (String s : list) {
  System.out.println(s);
  if ("s1".equals(s)) {
  list.remove(s);
  }
}

// 2.iterator迭代,fail-safe,不会抛出异常
Iterator<String> iterator = list.iterator();
  while (iterator.hasNext()) {
  String s = iterator.next();
  System.out.println(s);
  if ("s1".equals(s)) {
  list.remove(s);
  }
}
相关文章
相关标签/搜索