fail-fast

WikiPedia

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

在系统设计中,快速故障系统是一种立即在其接口上报告任何可能指示故障的情况的系统。快速故障系统通常旨在停止正常运行,而不是尝试继续可能存在缺陷的过程。这样的设计通常会在操作的几个点检查系统的状态,因此可以及早发现任何故障。快速故障模块的职责是检测错误,然后让系统的下一个最高级别处理错误。

fail-fast(快速失败)可以理解为是一种系统机制,一种错误检测机制。在Java集合框架中,如比较熟悉的ArrayList,当多个线程访问ArrayList实例时,其中一个线程或者多个线程对ArrayList进行结构上的修改如add或者remove操作时会抛出ConcurrentModificationException(并发修改异常)这种异常并不是每次都出现是随机的。

原理

ArrayList有一个变量modCount当对ArrayList的结构上发上修改时会使modCount++而当你利用Iterator迭代器去遍历ArrayList时会比较modCount!= expectedmodCountArrayList中有一个checkForComodification()来检查期望的修改值是否和真实的修改值一致不一致就会抛出异常。以下代码如果将list.toString注释掉程序是不会抛出异常的因为ArrayList继承AbstractCollection,toString方法是利用迭代器进行遍历,但是在迭代器中有对modCount的判断所以导致程序会抛出异常,所以日常遍历ArrayList的时推荐使用迭代器遍历,这样可以让程序正确的抛出异常。

补充

在遍历集合时失败的快速迭代器会立即抛出Concurrent Modification Exception如果集合有结构上的修改。因此,面对并发修改,迭代器将快速而干净地失败,而不是在未来的不确定时间内冒任意,不确定的行为的风险。

  • 快速失败的迭代器可以在两种情况下引发ConcurrentModificationException:

    • 单线程环境
      创建迭代器后,通过迭代器自己的remove方法以外的任何方法随时修改结构。

    • 多线程环境
      如果一个线程正在修改集合的结构,而另一个线程正在其上进行迭代。

如何避免

  • 在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。
  • 使用java并发包java.util.concurrent中的类来代替ArrayList
  • 使用Collections.synchronizedList()
public static void main(String[] args) {
        
        List<Integer> list = new ArrayList<>();
        
        new Thread(()->{
            for(int i=0;i<10;i++) {
                list.add(i);
            }
        }).start();
        
        new Thread(()->{
            list.remove(2);
        }).start();

        System.out.println(list.toString());
    }


public String toString() {
        Iterator<E> it = iterator();
        if (! it.hasNext())
            return "[]";

        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (;;) {
            E e = it.next();
            sb.append(e == this ? "(this Collection)" : e);
            if (! it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    }


final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

fail-safe

故障安全迭代器会复制内部数据结构(对象数组)并在复制的数据结构上进行迭代。对迭代器进行的任何结构修改都会影响复制的数据结构。因此,原始数据结构在结构上保持不变。因此,故障安全迭代器不会抛出ConcurrentModificationException

  • 与故障安全迭代器相关的两个问题是:

    • 1.维护复制的数据结构即内存的开销。

    • 2.故障安全迭代器不保证所读取的数据是原始数据结构中当前的数据。

根据Oracle文档,故障安全迭代器通常成本太高,但是在遍历操作远远超过变异时,其效率可能比替代方法高,并且在您无法或不想同步遍历而又需要防止并发线程之间的干扰时很有用。 “快照”样式的迭代器方法在创建迭代器时使用对数组状态的引用。该数组在迭代器的生命周期内永不更改,因此不会发生干扰,并且保证迭代器不会引发ConcurrentModificationException。自创建迭代器以来,迭代器将不会反映对列表的添加,删除或更改。不支持在迭代器本身上进行元素更改操作(remove(),set()和add())。这些方法引发UnsupportedOperationException

  • 故障快速迭代器和故障安全迭代器的示例
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class FailFastExample
{
    
    
    public static void main(String[] args)
    {
        Map<String,String> premiumPhone = new HashMap<String,String>();
        premiumPhone.put("Apple", "iPhone");
        premiumPhone.put("HTC", "HTC one");
        premiumPhone.put("Samsung","S5");
        
        Iterator iterator = premiumPhone.keySet().iterator();
        
        while (iterator.hasNext())
        {
            System.out.println(premiumPhone.get(iterator.next()));
            premiumPhone.put("Sony", "Xperia Z");
        }
        
    }
    
}
  • 输出
iPhone 
Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
        at java.util.HashMap$KeyIterator.next(Unknown Source)
        at FailFastExample.main(FailFastExample.java:20)
import java.util.concurrent.ConcurrentHashMap;
import java.util.Iterator;


public class FailSafeExample
{
    
    
    public static void main(String[] args)
    {
        ConcurrentHashMap<String,String> premiumPhone = 
                               new ConcurrentHashMap<String,String>();
        premiumPhone.put("Apple", "iPhone");
        premiumPhone.put("HTC", "HTC one");
        premiumPhone.put("Samsung","S5");
        
        Iterator iterator = premiumPhone.keySet().iterator();
        
        while (iterator.hasNext())
        {
            System.out.println(premiumPhone.get(iterator.next()));
            premiumPhone.put("Sony", "Xperia Z");
        }
        
    }
    
}

  • 输出
S5
HTC one
iPhone

两者之间的不同

Fail Fast IteratorFail Safe Iterator
Throw ConcurrentModification ExceptionYesNO
Clone objectNOYes
Memory OverheadLessMore
ExamplesHashMap,Vector,ArrayList,HashSetCopyOnWriteArrayList,ConcurrentHashMap