解析Java的迭代器中的fast-fail错误检测机制

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出改异常。
诚然,迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常,所以因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。

Java中的Iterator非常方便地为所有的数据源提供了一个统一的数据读取(删除)的接口,但是新手通常在使用的时候容易报如下错误ConcurrentModificationException,原因是在使用迭代器时候底层数据被修改,最常见于数据源不是线程安全的类,如HashMap & ArrayList等。

为什么要有fast-fail
一个案例
来一个新手容易犯错的例子:

String[] stringArray = {"a","b","c","d"};
List<String> strings = Arrays.asList(stringArray);
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()) {
 if(iterator.next().equals("c")) {
  strings.remove("c");
 }
}

更加常见的是在foreach(本质一样,都是调用Iterator时,操作了原始的strings)语句中:

for(String s : strings) {
 if(s.equals("c")) {
  strings.remove("c");
 }
}

产生原因
Java中的集合类(数据源)分为两种类型:线程安全,位于java.util.concurrent命名目录下,如CopyOnWriteArrayList;线程不安全:位于java.util目录下,如ArrayList,HashMap。所谓线程安全是在多线程环境下,这个类还能表现出和行为规范一致的结果,是否文绉绉的...自己google吧。

那既然我们可以有线程安全的集合替代品,那么为什么还要存在ArrayList等呢?因为线程安全的类通常需要通过各种手段去保持对数据访问的同步,所以通常来说效率会比较差。而如果使用者清楚自身使用场景不存在并发的场景,那么使用非线程安全的集合类在速度上有很大的优势。

如果开发者在使用时没有注意,将非线程安全的集合类用在了并发的场景下,比如线程A获取了ArrayList的iterator,然后线程B通过调用ArrayList.add()修改了ArrayList的数据,此时就有可能会抛出ConcurrentModificationException,注意,这里是有可能。那为啥上面的例子里面也会报这个错误呢?上面并不存在并发的情况,搂一眼源码吧。

Iterator源码分析
集合类中的fast-fail实现方式都差不多,我们以最简单的ArrayList为例吧。
ArrayList中会持有一个变量,声明为:
protected transient int modCount = 0;记录的是我们对ArrayList修改的次数,比如我们调用 add(),remove()等改变数据的操作时,会将modCount++。

我们通过ArrayList.iterator()返回的是一个实现了Iterator接口的ArrayListIterator:

private class ArrayListIterator implements Iterator<E> {

  //省略部分代码.......
  //初始化时,直接给expectedModCount赋ArrayList的修改次数
  private int expectedModCount = modCount;

  @SuppressWarnings("unchecked") public E next() {
      ............
    ArrayList<E> ourList = ArrayList.this;
    //简单比较一下当前iterator初始化时ArrayList.modCount的值
    //和现在的值是否一致,如果不相等,认为在获取了当前iterator之后
    //有别的位置(有可能是别的线程)修改了ArrayList,直接抛异常
    if (ourList.modCount != expectedModCount) {
      throw new ConcurrentModificationException();
    }
     ............
  }
}

原理很简单,构建Iterator时将当前ArrayList的modCount存起来,以后每一次next()时,判断ArrayList的modCount值是否有变化,如果有,则是在这个过程中有代码改变了数据(前面已经提及,只有调用add() remove()等才会去修改modCount的值)。
这也说明了为什么在例子里面我们并不是并发的场景也报错,因为我们调用ArrayList.remove()时改变了modCount的值。

但是这个东西意义有多大呢?在我看来它有点画蛇添足的嫌疑。因为在真正的并发场景下,这个fast-fail机制并不能真正即使发现另外线程访问并修改ArrayList中的数据。原因如下:

再看看modCount的定义protected transient int modCount = 0;。你没有看错,它就是一个普通的变量,那么在并发场景下由于共享对象的不可见性,有可能别的线程修改了ArrayList中的modCount,而iterator所在的线程却并没有读取到这个更新。HashMap在1.6以前确实是用了volatile来修饰了modCount来保证各个线程直接对modCount的可见性,但是在1.7里面把这个修饰去掉了,而且认为这是一个bug-->Java7去掉volatitle,可悲啊。。。原因嘛,就是JDK的开发者认为为了这么个破事而需要使用volatitle简直浪费效率。

就算是使用volatitle就完事大吉了吗?nono,举个最简单的例子,线程A获取了一个集合类的Iterator,线程B调用了集合类的add(),在add()还没有执行到modCount++时,线程A获取执行,并执行结束。在这种场景下,执行结果并不确定。对于ArrayList的Iterator来说,有可能会报一个数组越界的异常...

总结
fast-fail是JDK为了提示开发者将非线程安全的类使用到并发的场景下时,抛出一个异常,及早发现代码中的问题。但正如本文前面所述,这种机制却不能绝对正确地给出提示,而且老的JDK版本为了更好地支持这个机制还付出了一定的效率代价。

fast-fail存在的唯一价值可能就是给新手制造一些迷惑,给他深入探索的动力...嘿嘿

补充:

很多网上资料说在使用Iterator时是不能修改数据的,这样也并不完全准确。即便是支持fast-fail的Iterator本身也提供了remove()来删除当前遍历到的元素,例如:ArrayListIterator中的remove(),前面举的栗子改成如下即可:

while (iterator.hasNext()) {
 if(iterator.next().equals("c")) {
  iterator.remove("c");
 }
}
(0)

相关推荐

  • Java使用设计模式中迭代器模式构建项目的代码结构示例

    迭代器(Iterator)模式,又叫做游标(Cursor)模式.GOF给出的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节.   迭代器模式由以下角色组成: 迭代器角色(Iterator):迭代器角色负责定义访问和遍历元素的接口. 具体迭代器角色(Concrete Iterator):具体迭代器角色要实现迭代器接口,并要记录遍历中的当前位置. 容器角色(Container):容器角色负责提供创建具体迭代器角色的接口. 具体容器角色(Concre

  • java集合_浅谈Iterable和Iterator的区别

    1). Iterator是迭代器类,而Iterable是为了只要实现该接口就可以使用foreach,进行迭代. 2). Iterable中封装了Iterator接口,只要实现了Iterable接口的类,就可以使用Iterator迭代器了. 3). 集合Collection.List.Set都是Iterable的实现类,所以他们及其他们的子类都可以使用foreach进行迭代. 4). Iterator中和核心的方法next(),hasnext(),remove(),都是依赖当前位置,如果这些集合直

  • Java集合框架中迭代器Iterator解析

    Java里面的数组数据可以通过索引来获取,那么对象呢?也是通过索引吗?今天我们就来分析一下Java集合中获取集合对象的方法迭代-Iterator. 本篇文章主要分析一下Java集合框架中的迭代器部分,Iterator,该源码分析基于JDK1.8,分析工具,AndroidStudio,文章分析不足之处,还请指正! 一.简介 我们常常使用 JDK 提供的迭代接口进行 Java 集合的迭代. Iterator iterator = list.iterator(); while(iterator.has

  • 详解Java中的迭代迭代器Iterator与枚举器Enumeration

    迭代器Iterator接口 1.迭代器接口 Iterable 内置方法iterator(), 返回一个新建的 Iterator. 如: public interface Iterable { Iterator Iterator(); } Iterator 有 hasNext() 和 next() 两个方法要实现. public interface Iterator { boolean hasNext(); Item next(); void remove(); //可选实现 } 2.实现 导入

  • 浅谈java Iterator.remove()方法的用法(详解)

    实例如下: @Test public void tt(){ List<String> list = new ArrayList<String>(); list.add( "0" ); list.add( "1" ); list.add( "2" ); list.add( "3" ); list.add( "4" ); list.add( "5" ); list.a

  • Java设计模式之迭代模式(Iterator模式)介绍

    上了这么多年学,我发现一个问题,好象老师都很喜欢点名,甚至点名都成了某些老师的嗜好,一日不点名,就饭吃不香,觉睡不好似的,我就觉得很奇怪,你的课要是讲的好,同学又怎么会不来听课呢,殊不知:"误人子弟,乃是犯罪!"啊. 好了,那么我们现在来看老师这个点名过程是如何实现吧: 1.老规矩,我们先定义老师(Teacher)接口类: 复制代码 代码如下: public interface Teacher {     public Iterator createIterator(); //点名 }

  • java集合迭代器Iterator中的remove陷阱

    package TestList; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.TreeSet; public class TestIterator { /**      * @param args      */     public static void main(String[] args) {         // TODO Auto-gen

  • java使用iterator遍历指定目录示例分享

    可以遍历指定目录(一个或多个)及其所有子文件,用Iterator实现 复制代码 代码如下: package archiver.util; import java.io.File;import java.util.ArrayList;import java.util.Arrays;import java.util.Iterator;import java.util.List; public class FileIterator implements Iterator<File>, Iterabl

  • Java集合Iterator迭代的实现方法

    我们常常使用 JDK 提供的迭代接口进行 Java 集合的迭代. Iterator iterator = list.iterator(); while(iterator.hasNext()){ String string = iterator.next(); //do something } 迭代其实我们可以简单地理解为遍历,是一个标准化遍历各类容器里面的所有对象的方法类,它是一个很典型的设计模式.Iterator 模式是用于遍历集合类的标准访问方法.它可以把访问逻辑从不同类型的集合类中抽象出来

  • 使用迭代器模式来进行Java的设计模式编程

    定义:提供一种方法访问一个容器对象中各个元素,而又不暴露该对象的内部细节. 类型:行为类模式 类图: 如果要问java中使用最多的一种模式,答案不是单例模式,也不是工厂模式,更不是策略模式,而是迭代器模式,先来看一段代码吧: public static void print(Collection coll){ Iterator it = coll.iterator(); while(it.hasNext()){ String str = (String)it.next(); System.out

  • 详解Java中Iterator迭代器的用法

    迭代器(Iterator) 迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构.迭代器通常被称为"轻量级"对象,因为创建它的代价小. Java中的Iterator功能比较简单,并且只能单向移动: (1) 使用方法iterator()要求容器返回一个Iterator.第一次调用Iterator的next()方法时,它返回序列的第一个元素.注意:iterator()方法是java.lang.Iterable接口,被Collection继承

  • Java实现的自定义迭代器功能示例

    本文实例讲述了Java实现的自定义迭代器功能.分享给大家供大家参考,具体如下: 编写自己的Iterator,实现Iterator接口,这里多说一句,实现Iterable后,可以用"foreach"循环遍历你的对象. import java.util.Iterator; import java.util.NoSuchElementException; /** * 演示Iterator和Iterable接口,并说明怎样编写一个用于对象数组的简单迭代器. */ public class Aa

  • 轻松掌握Java迭代器模式

    定义:用于顺序访问集合对象的元素,不需要知道集合对象的底层表示. 特点: 1.它支持以不同的方式遍历一个聚合对象. 2.迭代器简化了聚合类. 3.在同一个聚合上可以有多个遍历. 4.在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码. 企业级开发和常用框架中的应用:java集合都实现了迭代器 具体实例: public class Demo { public static void main(String[] args) { ActualContainer container =

随机推荐