声明

  特点:基于JDK源码进行分析。

  研究费时费力,如需转载或摘要,请显著处注明出处,以尊重劳动研究成果:博客园 - https://www.cnblogs.com/johnnyzen/p/10547179.html,侵权必究,蟹蟹理解。

/**
* @IDE: Created by IntelliJ IDEA.
* @Author: 千千寰宇
* @Email: 1125418540@qq.com
* @Date: 2019/3/17 12:46:34
* @Description: Java SE之Fast-Fail与Fast-Safe的区别[集合与多线程/增强For]
*/

[1] Fast-Fail事件

特征

  ① java.util包下面的所有的集合类

+ ArrayList / HashMap / LinkedList / Iterator / Collections / …

  ②会抛出ConcurrentModificationException异常,一种错误检测机制

  ③产生条件:其一,当多个线程对Collection下的集合类进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;其二,单线程环境下,如果单次遍历对该包下的集合对象进行多次写操作,则也可能抛出ConcurrentModificationException异常。

  ④解决办法:通过util.concurrent包下的相应类去处理,则:不会产生fast-fail事件,即fast-safe

案例分析

  【测试源码】

public static void arrayList(){
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
for(Object obj : list){
System.out.println(obj);
list.remove(obj); //使用集合对象list删除元素
}
}
【运行结果】

【原因】
//ArrayList的内部数组
transient Object[] elementData;
//ArrayList的iterator()方法
public Iterator<E> iterator() {
return new Itr();
}
//ArrayList的remove()方法
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index); //关键调用处:修改 modCount++
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);//关键调用处:修改 modCount++
return true;
}
}
return false;
}
//ArrayList的fastRemove()方法
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//对list对象内部数组的元素进行删除,但会修改modCount值
elementData[--size] = null; // clear to let GC do its work
} //ArrayList 的私有内部类Itr implements Iterator的next()
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();//检查modCount与expectedModCount
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
//对list对象内部数组的元素进行删除,但不会修改modCount值
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;//即 使用ArrayList对象获取的iterator对象的remove方法删除元素时,将设置expectedModCount = modCount ,以确保下一次能通过checkForComodification()
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}//Itr.remove() end
}//Itr end
1.Java.util包下的集合类(如:ArrayList)的迭代器(如:私有内部类Itr)在遍历时直接访问集合中的内容,并且在遍历过程中使用了modCount变量
2.Java.util包下的集合类(如:ArrayList)的集合对象中在被遍历期间如果内容发生变化,就会改变modCount的值(如:ArrayList的remove())
3.每当集合类(如:ArrayList)对象的迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测modCount变量和expectedmodCount值是否相等
4.如果相等就返回遍历结果,否则抛出异常,终止遍历
(整个分析过程,可参见上述ArrayList中源码)引用于[2]
【另外:关于ArrayList.remove()与其ArrayList.iterator().reomve()的区别?】
上述案例的源码中使用 Collection(ArrayList 的父类) 集合对象list中的 remove()方法。该方法只能从集合中删除元素,不能把迭代器iterator中的元素也删除了。即:
ArrayList.remove()
删除元素时,修改modCount++
ArrayList.Itr.remove()
删除元素时,调用ArrayList.remove()后,立即设置modCount=expectedmodCount,
避免了并发修改异常(ConcurrentModificationException)。
 
值得注意的是:两方法均是对同一对象的同一动态数组(elementData)进行操作。那么,在多线程环境下,如果多个线程对同一list对象进行操作时,elementData便是实际上的共享互斥资源,则 可能造成数据污染的非线程安全事件。
 
【产生问题】
1.【单线程环境】当使用foreach循环时,欲实现对util包下的集合对象一边遍历一边增、删集合元素时,该怎么办?
答:其一,每次遍历进行单次写操作,通过集合对象的迭代器iterator对应增、删元素(Itr.remove())的方法,线程安全。
//demo 不会抛并发修改异常
Iterator it = list.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
if(obj.equals(1)){
it.remove();//使用迭代器的remove()方法
}
} 
  其二,每次遍历进行多次写操作,不一定线程安全。原因是:并发修改异常触发时,必须满足checkForComodification()方法的modCount == expectedmodCount的成立。那么,如果集合发生变化时修改modCount值, 刚好有设置为了expectedmodCount值, 则异常不会抛出。
  比如:先删除一条数据,再添加一条数据。
//此时,不会抛并发修改异常
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
for(Object obj : list){
System.out.println(obj);
if(obj == new Integer(2)){ //使用equals()也会抛并发修改异常
list.remove(obj);
list.add(2);
}
}

   2.【多线程环境】当多个线程同时对util包下的同一集合对象进行写操作时,是否线程安全?

  答:不能依赖于ConcurrentModificationException异常是否抛出,而进行并发操作的编程, 该异常只检测并发修改的BUG。

【结论】

  java.util包下的集合类都是快速失败(fail-fast)机制的, 不能在多线程下发生并发修改,在单线程下,也不推荐迭代过程中修改该集合类对象的元素值。引用于[2]

[2] Fast-Safe事件

  ①java.util.concurrent包下面的所有的类

+ BlockingDeque / LinkedBlockingDeque / SynchronousQueue

+ ConcurrentHashMap / ConcurrentLinkedQueue / CopyOnWriteArrayList

+ …

不会抛出并发修改异常;采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

       ③结论:java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。 引用于[2]

  ④原理:由于迭代时是对原集合的拷贝的值进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会出发ConcurrentModificationExceptio

  ⑤缺陷:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地, 迭代器并不能访问到修改后的内容 (简单来说就是, 迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的) 引用于[2]

[3] 补充:增强for

特征

  ①增强型for | For/in | Foreach循环语法:for(ElementType element:arrayName){}

  ②不支持遍历时修改。详见上述fast-fail中关于“每次遍历单/多次修改”。

  ③对遍历的集合需要做null判断,不然可能引发空指针异常

  ④增强for循环的内部:

    遍历数组,foreach 循环实际上还是用的普通的 for 循环 引用[1]

    遍历集合,foreach 循环实际上是用的 iterator 迭代器迭代 引用[1]

  备注:始于JDK1.5

案例分析

【测试源码】

//实验
public class Foreach {
public static void foreach(){
int [] array = {1,2,3};
for(int i : array){
System.out.println(i);
}
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
for(Object obj : list){
System.out.println(obj);
}
}
}

【反编译结果】javap -c sourceFile

[4] 参考文献

  [1] Java中的增强 for 循环/ foreach

   备注:该大V博文关于fast-fail与fast-safe的探究,认为是增强for循环基于多线程是错误的,毫无依据,JDK源码中也并未体现。

  [2]【荐】fail-fast和fail-safe的介绍和区别

  [3] fail-fast和fail-safe详解

  [4]【荐】Concurrent下的线程安全集合

Java SE之快速失败(Fast-Fail)与快速安全(Fast-Safe)的区别[集合与多线程/增强For](彻底详解)的更多相关文章

  1. 快速失败(fail—fast)和 安全失败(fail—safe)

    快速失败(fail-fast) 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加.删除),则会抛出Concurrent Modification Exception. 原理 ...

  2. Java集合框架中的快速失败(fail—fast)机制

      fail-fast机制,即快速失败机制,是java集合框架中的一种错误检测机制.多线程下用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加.删除),则会抛出Concurre ...

  3. Java集合【2】--iterator接口详解

    目录 一.iterator接口介绍 二.为什么需要iterator接口 三.iterator接口相关接口 3.1 ListIterator 3.2 SpitIterator 3.2.1 SpitIte ...

  4. Java中String的intern方法,javap&cfr.jar反编译,javap反编译后二进制指令代码详解,Java8常量池的位置

    一个例子 public class TestString{ public static void main(String[] args){ String a = "a"; Stri ...

  5. Java多线程之线程池详解

    前言 在认识线程池之前,我们需要使用线程就去创建一个线程,但是我们会发现有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因 ...

  6. java集合HashMap、HashTable、HashSet详解

    一.Set和Map关系 Set代表集合元素无序,集合元素不可重复的集合,Map代表一种由多个key-value组成的集合,map集合是set集合的扩展只是名称不同,对应如下 二.HashMap的工作原 ...

  7. java Spring系列之 配置文件的操作 +Bean的生命周期+不同数据类型的注入简析+注入的原理详解+配置文件中不同标签体的使用方式

    Spring系列之 配置文件的操作 写在文章前面: 本文带大家掌握Spring配置文件的基础操作以及带领大家理清依赖注入的概念,本文涉及内容广泛,如果各位读者耐心看完,应该会对自身有一个提升 Spri ...

  8. java多线程——同步块synchronized详解

    Java 同步块(synchronized block)用来标记方法或者代码块是同步的.Java同步块用来避免竞争.本文介绍以下内容: Java同步关键字(synchronzied) 实例方法同步 静 ...

  9. Java基础学习(四)-- 接口、集合框架、Collection、泛型详解

    接口 一.接口的基本概念 关键字为:Interface,在JAVA编程语言中是一个抽象类型,是抽象方法的集合.也是使用.java文件编写.   二.接口声明 命名规范:与类名的命名规范相同,通常情况下 ...

随机推荐

  1. UI自动化之日志

    Python自动化测试中,日志输出功能是不能缺少的一部分.让我们来看看如何实现日志的输出吧 一.控制台输出日志 def get_logger(): try: if not os.path.exists ...

  2. 项目中遇到angular时间插件datetinepicker汉化问题

    问题描述: 测试需要中文的时间插件: 参考资料: angularjs封装bootstrap官网的时间插件datetimepicker https://www.cnblogs.com/cynthia-w ...

  3. 配置sonarqube与gitlab sso认证集成

    1.安装插件 sonar插件地址:https://github.com/gabrie-allaigre/sonar-auth-gitlab-plugin 安装插件: 下载插件然后通过maven打包然后 ...

  4. 在Winform开发框架中对附件文件进行集中归档处理

    在我们Winform开发中,往往需要涉及到附件的统一管理,因此我倾向于把它们独立出来作为一个附件管理模块,这样各个模块都可以使用这个附件管理模块,更好的实现模块重用的目的.在涉及附件管理的场景中,一个 ...

  5. php将字符串转为二进制数据串

    /** * 将字符串转换成二进制 * @param type $str * @return type */ function StrToBin($str){ //1.列出每个字符 $arr = pre ...

  6. html5+ 原生标题栏添加input 输入框

    titleNView: { backgroundColor: "#f7f7f7", // 导航栏背景色 titleText: "", // 导航栏标题 titl ...

  7. 浅析Java的Object类

    前言:   最近在回顾Java基础,在此过程中,查看源码是少不了的   这里以JDK8以基准,记录一些自己查看源码的观感 Object类,翻阅源码,看看这个类的所在位置,是在 java.lang 包下 ...

  8. LODOP获取打印成功,是否加入队列

    之前博文介绍过获取打印机状态码 LODOP获取打印机状态码和状态码含义测试,但是打印机种类千差万别,状态码不一定准确,特别是打印成功的状态码,获取任务不在队列,可以判断打印成功,删除任务也是任务不在队 ...

  9. Spring Boot 2.x 编写 RESTful API (二) 校验

    用Spring Boot编写RESTful API 学习笔记 约束规则对子类依旧有效 groups 参数 每个约束用注解都有一个 groups 参数 可接收多个 class 类型 (必须是接口) 不声 ...

  10. JS 基础知识点

    最近发现一个好东西,掘金小册,觉得里面的东西挺不错的,准备仔细阅读一下,提升下自己. 记录一下,随便加深点儿印象,主要内容源自于小册. 原始类型 原始类型也成为基本数据类型 boolean null ...