首页 > 代码库 > java Iterator Fail-fast机制

java Iterator Fail-fast机制

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

 

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

 

java Iterator Fail-fast机制