大杂烩 -- Java中Iterator的fast-fail分析

基础大杂烩 -- 目录

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获取了ArrayListiterator,然后线程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时将当前ArrayListmodCount存起来,以后每一次next()时,判断ArrayListmodCount值是否有变化,如果有,则是在这个过程中有代码改变了数据(前面已经提及,只有调用add() remove()等才会去修改modCount的值)。
这也说明了为什么在例子里面我们并不是并发的场景也报错,因为我们调用ArrayList.remove()时改变了modCount的值。

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

  1. 再看看modCount的定义protected transient int modCount = 0;。你没有看错,它就是一个普通的变量,那么在并发场景下由于共享对象的不可见性,有可能别的线程修改了ArrayList中的modCount,而iterator所在的线程却并没有读取到这个更新。HashMap在1.6以前确实是用了volatile来修饰了modCount来保证各个线程直接对modCount的可见性,但是在1.7里面把这个修饰去掉了,而且认为这是一个bug-->Java7去掉volatitle,可悲啊。。。原因嘛,就是JDK的开发者认为为了这么个破事而需要使用volatitle简直浪费效率。
  2. 就算是使用volatitle就完事大吉了吗?nono,举个最简单的例子,线程A获取了一个集合类的Iterator,线程B调用了集合类的add(),在add()还没有执行到modCount++时,线程A获取执行,并执行结束。在这种场景下,执行结果并不确定。对于ArrayListIterator来说,有可能会报一个数组越界的异常...

总结

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

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

补充:

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

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

啦啦啦

时间: 2024-10-12 22:12:30

大杂烩 -- Java中Iterator的fast-fail分析的相关文章

畅销书对Java中Iterator的理解误区

声明:本博客为原创博客,未经允许,不得转载!原文链接为http://blog.csdn.net/bettarwang/article/details/28110615 最近放假,闲来无事,便翻看以前看过的一些书,竟然发现有些书本(甚至是一些畅销书)对Java中Iterator有很大的误解,比如某畅销书在Collection那一章有这么一句话:"当使用Iterator对集合元素进行迭代时,Iterator并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集

Java中arraylist和linkedlist源代码分析与性能比較

Java中arraylist和linkedlist源代码分析与性能比較 1,简单介绍 在java开发中比較经常使用的数据结构是arraylist和linkedlist,本文主要从源代码角度分析arraylist和linkedlist的性能. 2,arraylist源代码分析 Arraylist底层的数据结构是一个对象数组.有一个size的成员变量标记数组中元素的个数,例如以下图: * The array buffer into which the elements of the ArrayLis

JAVA学习(六):JAVA中的继承及其常见问题分析

JAVA中的继承及其常见问题分析 1.JAVA中继承的定义 JAVA中,类的继承是通过扩展其他类而形成新类来实现的,原来的类称为父类(Super Class)或基类,新的类称为原来类的子类或派生类.在子类中,不仅包含了父类的属性和方法,还可以增加新的属性和方法,从而使得父类的基本特征可被所有子类对象共享. 注:类的继承并不改变类成员的访问权限,也就是说,如果父类的成员是公有的.被保护的或默认的,它的子类仍具有相应的这些特性. /**********************************

Java中Iterator(迭代器)的用法及其背后机制的探究

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

Java中String连接性能的分析

总结:如果String的数量小于4(不含4),使用String.concat()来连接String,否则首先计算最终结果的长度,再用该长度来创建一个StringBuilder,最后使用这个StringBuilder来连接所有String.      我建议大家如果确定需要连接的String的数量小于4的,直接使用String.concat()来连接,虽然StringBundler能够帮你自动处理这一情况,但创建一个String[]和那些方法调用都是一些无谓的开销. Java中的String是一个

Java中Iterator用法整理

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

Java中Iterator(迭代器)的用法及其背后机制探究

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

JAVA中Iterator的具体作用

Iterator是对集合进行迭代的迭代器 作用是将集合中的元素遍历,取出来 举个例子: import java.util.ArrayList; import java.util.Iterator; public class Test { public static void main(String argv[]) { ArrayList list = new ArrayList(); list.add(1); list.add(2); list.add(3 ); Iterator it = li

Java中CAS底层实现原理分析

CAS(无锁优化.自旋锁)原理分析 一.CAS(compareAndSwap)的概念 CAS,全称Compare And Swap(比较与交换),解决多线程并行情况下使用锁造成性能损耗的一种机制. CAS(V, A, B),V为内存地址.A为预期原值,B为新值.如果内存地址的值与预期原值相匹配,那么将该位置值更新为新值.否则,说明已经被其他线程更新,处理器不做任何操作:无论哪种情况,它都会在 CAS 指令之前返回该位置的值.而我们可以使用自旋锁,循环CAS,重新读取该变量再尝试再次修改该变量,也