大杂烩 -- 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获取了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");
}
}
啦啦啦
大杂烩 -- Java中Iterator的fast-fail分析的更多相关文章
- Java中arraylist和linkedlist源代码分析与性能比較
Java中arraylist和linkedlist源代码分析与性能比較 1,简单介绍 在java开发中比較经常使用的数据结构是arraylist和linkedlist,本文主要从源代码角度分析arra ...
- Java中Iterator类的详细介绍
迭代器模式:就是提供一种方法对一个容器对象中的各个元素进行访问,而又不暴露该对象容器的内部细节. 概述 Java集合框架的集合类,我们有时候称之为容器.容器的种类有很多种,比如ArrayList.Li ...
- Java中Iterator的fast-fail分析
1.fail-fast简介 fail-fast机制是java集合(Collection)中的一个错误机制.当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件. 例如:当某一个线 ...
- Java 中Iterator 、Vector、ArrayList、List 使用深入剖析
标签:Iterator Java List ArrayList Vector 线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些 ...
- Java中Iterator(迭代器)的用法及其背后机制的探究
在Java中遍历List时会用到Java提供的Iterator,Iterator十分好用,原因是: 迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结 ...
- Java中String连接性能的分析【转】
[转]http://www.blogjava.net/javagrass/archive/2010/01/24/310650.html 总结:如果String的数量小于4(不含4),使用String. ...
- Java中Iterator(迭代器)实现原理
在Java中遍历List时会用到Java提供的Iterator,Iterator十分好用,原因是: 迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结 ...
- Java中String连接性能的分析
总结:如果String的数量小于4(不含4),使用String.concat()来连接String,否则首先计算最终结果的长度,再用该长度来创建一个StringBuilder,最后使用这个String ...
- Java中Iterator用法整理
迭代器(Iterator) 迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构.迭代器通常被称为“轻量级”对象,因为创建它的代价小. Java中的I ...
随机推荐
- drupal7 的核心模块
核心模块 Drupal 7 block Block(区块)模块提供了与区块相关的功能,通过区块可将内容放置在网站不同区域.Block模块是Drupal的基础模块之一,不能被禁用.它是通过单独的区块管理 ...
- android开发(43) 动画演示,会跑的小人,从屏幕左侧跑到右侧
想做一个动画,一个会跑的小人,从屏幕右侧跑道右侧,于是做了个尝试,上图: 要完成这样需要三步: 1. 做一个 帧动画 (frame animation),由多张图片组成,组成小人连续跑动的样子. 2. ...
- SpringBoot的文件下载
SpringBoot的文件下载 2017年11月29日 10:32:20 阅读数:3907 SpringBoot的文件下载方法有很多,此处只记录使用Spring的Resource实现类FileSyst ...
- Windows下超强日志工具BareTail
最近写了一些测试工具,经常希望能动态的查看日志文件,试了好些文本编辑文件如Emeditor.Vim等,一直没找到合适. 我希望支持的功能有: 支持大文件读取,速度要快 自动加载新的内容,直接定位到最后 ...
- 基于octree的空间划分及搜索操作
(1) octree是一种用于管理稀疏3D数据的树形数据结构,每个内部节点都正好有八个子节点,介绍如何用octree在点云数据中进行空间划分及近邻搜索,实现“体素内近邻搜索(Neighbors wi ...
- R语言日期的表示和运算(详细总结)
1.取出当前日期 Sys.Date() [1] "2014-10-29" date() #注意:这种方法返回的是字符串类型 [1] "Wed Oct 29 20:36: ...
- CSS3圆角,阴影,透明
CSS实现圆角,阴影,透明的方法很多,传统的方法都比较复杂,用CSS3就方便很多了,虽然现在各浏览器对CSS3的支持还不是很好,但不久的将来CSS3就会普及. 1.圆角 CSS3实现圆角有两种方法. ...
- Sword STL之map效率问题
#include <iostream> #include <map> #include <vector> using namespace std; /* STL容器 ...
- 阐述:SIP协议是什么
sip协议是什么?可能刚刚接触这个协议的朋友会掌握不好它的定义.那么首先我们要了解一下,目前企业中大多数VoIP应用都使用H.323协议,但是,随着越来越多的企业研究SIP协议,不久的将来基于SIP协 ...
- MBR:主引导记录:
下面内容严重参考:百度百科: Main Boot Record)是位于磁盘最前边的一段引导(Loader)代码.它负责磁盘操作系统(DOS)对磁盘进行读写时分区合法性的判别.分区引导信息的定位,它由磁 ...