java Iterator Fail-fast机制

时间:2021-10-14 12:40:17

Fail-fast:在迭代的过程中发现数据被改变时立即抛出异常,而不是等遍历完了再抛出异常;可以理解为快速感知。

在并发的时候,当线程A正遍历一个Collection或Map,这时另外一个线程B修改Collection或Map,线程A就会抛出一个错:ConcurrentModificationException。

即使是在单线程下运行, java.util.ConcurrentModificationException 异常也将被抛出。

表明:我正读取的内容被修改掉了,你是否需要重新遍历?或是做其它处理?这就是fail-fast的含义。

Map<Integer, String> map = new HashMap<>();
map.put(1, "a");
map.put(2, "a"); Iterator<Integer> keys = map.keySet().iterator(); while (keys.hasNext()) {
Integer i = 0;
try {
i = keys.next();
System.out.println("before remove: "+map.size());
/**
* Removes from the underlying collection the last element returned by this iterator (optional operation).
* This method can be called only once per call to next().
* The behavior of an iterator is unspecified if the underlying collection is modified while the iteration
* is in progress in any way other than by calling this method.
* */
//keys.remove();
map.remove(i); System.out.println("remove running: "+map.size());
} catch (ConcurrentModificationException e) {
System.out.println(e.toString());
System.exit(0);
}

  当使用 fail-fast iterator 对 Collection 或 Map 进行迭代操作过程中尝试直接修改 Collection / Map 的内容时,即使是在单线程下运行, java.util.ConcurrentModificationException 异常也将被抛出。

Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭 代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。

所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象, Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。

有意思的是如果你的 Collection / Map 对象实际只有一个元素的时候, ConcurrentModificationException 异常并不会被抛出。这也就是为什么在 javadoc 里面指出: it would be wrong to write a program that depended on this exception for its correctness: ConcurrentModificationException should be used only to detect bugs.

java.util 包中的集合类都返回 fail-fast 迭代器,这意味着它们假设线程在集合内容中进行迭代时,集合不会更改它的内容。如果 fail-fast 迭代器检测到在迭代过程中进行了更改操作,那么它会抛出 ConcurrentModificationException ,这是不可控异常。 
      在迭代过程中不更改集合的要求通常会对许多并发应用程序造成不便。相反,比较好的是它允许并发修改并确保迭代器只要进行合理操作,就可以提供集合的一致视图,如 java.util.concurrent 集合类中的迭代器所做的那样。 
     java.util.concurrent 集合返回的迭代器称为弱一致的(weakly consistent) 迭代器。对于这些类,如果元素自从迭代开始已经删除,且尚未由 next() 方法返回,那么它将不返回到调用者。如果元素自迭代开始已经添加,那么它可能返回调用者,也可能不返回。在一次迭代中,无论如何更改底层集合,元素不会被 返回两次。

Fail-fast是并发中乐观(optimistic)策略的具体应用,它允许线程*竞争,但在出现冲突的情况下假设你能应对,即你能判断出问题何在,并且给出解决办法。悲观(pessimistic)策略就正好相反,它总是预先设置足够的限制,通常是采用锁(lock),来保证程序进行过程中的无错,付出的代价是其它线程的等待开销:

Vector v = …;

synchronized(v){

ListIterator iter = v.listIterator();

while(iter.hasNext()) {

Object o = iter.next();

//do something 

...

}

}

  

可以瞧见,这个遍历过程被同步,当然不会有其它线程能修改v(即使Vector本身就是同步的),决不会出现错乱、无法预知的结果。但这个同步块若是一个被频繁调用的模块,则其它线程等待的时间不会是个小数字,这时候,它就是一个单线程系统,可就看CPU的处理能力了:)

Doug Lea的concurrent包给出了另外一种解决方案,Copy On Write。它的CopyOnWriteArrayList、CopyOnWriteArraySet会在线程B试图修改数据容器时,给出一个copy出来的容器(当然,我们程序中是感觉不到的),这样线程A在老版本的v上遍历,线程B则在新版本的v上修改,两者互不相干,也决不会出现ConcurrentModificationException。它的代价则主要是在容器的copy上,当并发程度越高,其开销也越高。

所以,Fast Fail被引入JDK的一个基本前提是:你绝大多数的情形,仅仅是在遍历一个collection,不会有另外的线程会对它做update。如此,它的效率是最充分的。但如果你不断遇到ConcurrentModificationException的异常时,则要考虑是否要进行一定次数的重新遍历,或者干脆采用悲观策略锁住资源来保证线程安全。

三、fail-fast解决办法

通过前面的实例、源码分析,我想各位已经基本了解了fail-fast的机制,下面我就产生的原因提出解决方案。这里有两种解决方案:

        方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

        方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。

CopyOnWriteArrayList为何物?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。2:当遍历操作的数量大大超过可变操作的数量时。遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。那么为什么CopyOnWriterArrayList可以替代ArrayList呢?

第一、CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。

第二、CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。请看:

  1. private static class COWIterator<E> implements ListIterator<E> {
  2. /** 省略此处代码 */
  3. public E next() {
  4. if (!(hasNext()))
  5. throw new NoSuchElementException();
  6. return this.snapshot[(this.cursor++)];
  7. }
  8. /** 省略此处代码 */
  9. }

CopyOnWriterArrayList的方法根本就没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等。它为什么会这么做,凭什么可以这么做呢?我们以add方法为例:

  1. public boolean add(E paramE) {
  2. ReentrantLock localReentrantLock = this.lock;
  3. localReentrantLock.lock();
  4. try {
  5. Object[] arrayOfObject1 = getArray();
  6. int i = arrayOfObject1.length;
  7. Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
  8. arrayOfObject2[i] = paramE;
  9. setArray(arrayOfObject2);
  10. int j = 1;
  11. return j;
  12. } finally {
  13. localReentrantLock.unlock();
  14. }
  15. }
  16. final void setArray(Object[] paramArrayOfObject) {
  17. this.array = paramArrayOfObject;
  18. }

CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不同点就在于,下面三句代码:

  1. Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
  2. arrayOfObject2[i] = paramE;
  3. setArray(arrayOfObject2);

就是这三句代码使得CopyOnWriterArrayList不会抛ConcurrentModificationException异常。他们所展现的魅力就在于copy原来的array,再在copy数组上进行add操作,这样做就完全不会影响COWIterator中的array了。

所以CopyOnWriterArrayList所代表的核心概念就是:任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。