Java - Java集合中的快速失败Fail Fast 机制

简介: Java - Java集合中的快速失败Fail Fast 机制

3243d483b2654448b9eb9bb0ef3f6656.png


什么是 fail-fast


https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html


3371f4beab084094bb3bc989bcb55c63.png

0b6f69e01d224c56b53c959cc42f31a8.png


fail-fast 机制是Java集合(Collection)中的一种错误机制。


在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加、删除),则会抛出Concurrent Modification Exception 【并发修改异常】。


举个例子:


在多线程环境下,线程1正在对集合进行遍历,此时线程2对集合进行修改 , 很容易抛出Concurrent Modification Exception 。


当然了,在单线程的情况下,遍历时对集合进行修改也会抛出Concurrent Modification Exception


此类的返回的迭代器iterator和 listIterator方法是快速失败的:如果列表在任何时间后,迭代器创建结构修饰,以任何方式除非通过迭代器自身 remove或 add方法,迭代器都将抛出 Concurrent Modification Exception。


因此,面对并发修改,迭代器快速而干净地失败,而不是冒着在未来不确定的时间出现任意、非确定性行为的风险。


源码解读


Itr


在遍历的时候对集合修改会发生fail-fast,遍历集合------> 迭代器


5193888993de40ad87f116e3c53c0a36.png

/**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        Itr() {}
        public boolean hasNext() {
            return cursor != size;
        }
        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            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];
        }
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

看到了吧, checkForComodification

 final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }


在modCount != expectedModCount的时候抛出了ConcurrentModificationException,


而在next方法中上来就是调用checkForComodification,所以遍历集合才会可能抛出并发修改异常。


那接下来就研究 modCount 和 expectedModCount 什么时候会不相等就行了呗。


在创建一个迭代器后,expectedModCount的初始值就是modCount了,


75f638d8cf5a4bd4b737a80fe943a03f.png


  • 对集合修改会改变modCount


484eb66ee5b04b5591af28dd7dc89a76.png

  • expectedModCount只会在迭代器的remove方法中被修改为modCount


1a5aa771eb7242dba5455d66faa5537e.png


这都是 02784bcc78b1491896b1f4c5aa06dee6.png

中的内容,除了modCount 。 modCount 是ArrayList的常量,默认值 为0


为什么对集合的结构进行修改会发生并发修改异常-源码分析


那我们说,在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加、删除),则会抛出Concurrent Modification Exception 【并发修改异常】。


修改方法之 remove


0684274374bd4dc9a5aa441b775fa180.png

49024b7bf0de4241be65c320d093aade.png


modCount++ , 后面modCount会和expectedModCount不相等,进而抛出并发修改异常。

62cde2c70cbb44c784f41ec4dbc6ed79.png

修改方法之 add

d89ba4215a3d4916bbb50d83c533effe.png

c6ea16d53a6a4300b875e18c333e47ae.png


ensureCapacityInternal方法里对modCount++操作, 改变了modCount的值,所以调用

0d1f842945144a569195ba8e04ad1f9f.png


那set方法会触发 fast fail吗?

答案是不会。

ff578ef92e0342f3a519a123f1557f39.png


set没有对modCount++,所以对集合的某个元素进行修改并不会fail-fast


案例分享

【案例一】

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String tmp = iter.next();
    System.out.println(tmp);
    if (tmp.equals("1")) {
        list.remove("1");
    }
}


 1
Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
  at java.util.ArrayList$Itr.next(ArrayList.java:861)
  at com.artisan.fastfail.FastFailTest.main(FastFailTest.java:25)

调用了 list# remove方法


【案例二】

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String tmp = iter.next();
    System.out.println(tmp);
    if (tmp.equals("3")) {
        list.remove("3");
    }
}
1
2
3

调用了 list# remove方法 , 居然没有抛出并发修改异常????



remove倒数第二个元素,然而这时就没有抛出异常了 。 再分析分析吧


9aea1d1dae9a4044835ec3f4515c1208.png


cursor是下一个要返回的变量的下标

lastRet是上一个返回过的变量的下标


d2eac41c6118433a9792f765052f066e.png


hasNext方法告诉我们只有在下一个变量的下标不等于size的时候会告诉我们集合还有下一个元素。


但是在remove的时候,size- -了,那么删除“3”这个元素后,size变为3,而此时cursor也是3,那么再走到hasNext时,就发现cursor和size相等了,那么就会退出遍历,“4”压根就不会被遍历到。


所以没有抛出异常,因为remove后就退出了,还没来得及走到next方法呢~


【案例三】

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String tmp = iter.next();
    System.out.println(tmp);
    if (tmp.equals("4")) {
        list.remove("4");
    }
}


1
2
3
4
Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
  at java.util.ArrayList$Itr.next(ArrayList.java:861)
  at com.artisan.fastfail.FastFailTest.main(FastFailTest.java:25)


接上个案例


那 删除“4”,也就是最后一个元素,按理说删了最后一个元素不就退出了吗?走不到下一次的next方法呀?


其实是不对的,删完“4”并没有就直接退出 ! remove后size变成了3,但此时cursor是4,那么走到hasNext时,发现4!=3,就会再次进入循环,那么结果…走到了next方法,抛出了异常。。。


【案例四】

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
for (String i : list) {
    if ("1".equals(i)) {
        list.remove("1");
    }
}


Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
  at java.util.ArrayList$Itr.next(ArrayList.java:861)
  at com.artisan.fastfail.FastFailTest.main(FastFailTest.java:22)

用增强for循环遍历的,反编译class , 和用迭代器实质是一样的 。


【案例五】


List<String> list = Arrays.asList("1", "2", "3", "4");
for (String i : list) {
    if ("1".equals(i)) {
        list.remove("1");
    }
}


Exception in thread "main" java.lang.UnsupportedOperationException
  at java.util.AbstractList.remove(AbstractList.java:161)
  at java.util.AbstractList$Itr.remove(AbstractList.java:374)
  at java.util.AbstractCollection.remove(AbstractCollection.java:293)
  at com.artisan.fastfail.FastFailTest.main(FastFailTest.java:21)

用了Array.asList()方法生成的集合,抛出的是UnsupportedOperationException,发现asList生成的ArrayList是个静态内部类,并非java.util.ArrayList, 并没有这些方法。


所以不能对asList生成的ArrayList进行增删改


Java开发规范01 - 集合篇_Arrays.asList 坑


【案例六】

    List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator<String> iter = list.iterator();
        while (iter.hasNext()) {
            String tmp = iter.next();
            System.out.println(tmp);
            if (tmp.equals("1")) {
                iter.remove();
            }
        }
1
2
3
4


【案例七】

```java
// Java code to illustrate
// Fail Fast Iterator in Java
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class FailFastExample {
  public static void main(String[] args)
  {
    Map<String, String> cityCode = new HashMap<String, String>();
    cityCode.put("Delhi", "India");
    cityCode.put("Moscow", "Russia");
    cityCode.put("New York", "USA");
    Iterator iterator = cityCode.keySet().iterator();
    while (iterator.hasNext()) {
      System.out.println(cityCode.get(iterator.next()));
      // adding an element to Map
      // exception will be thrown on next call
      // of next() method.
      cityCode.put("Istanbul", "Turkey");
    }
  }
}


India
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextNode(HashMap.java:1442)
    at java.util.HashMap$KeyIterator.next(HashMap.java:1466)
    at FailFastExample.main(FailFastExample.java:18)
// Java code to demonstrate remove
// case in Fail-fast iterators
import java.util.ArrayList;
import java.util.Iterator;
public class FailFastExample {
  public static void main(String[] args)
  {
    ArrayList<Integer> al = new ArrayList<>();
    al.add(1);
    al.add(2);
    al.add(3);
    al.add(4);
    al.add(5);
    Iterator<Integer> itr = al.iterator();
    while (itr.hasNext()) {
      if (itr.next() == 2) {
        // will not throw Exception
        itr.remove();
      }
    }
    System.out.println(al);
    itr = al.iterator();
    while (itr.hasNext()) {
      if (itr.next() == 3) {
        // will throw Exception on
        // next call of next() method
        al.remove(3);
      }
    }
  }
}
[1, 3, 4, 5]
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at FailFastExample.main(FailFastExample.java:28)

阿里巴巴Java开发手册中的规定


8ea7bad163df4f0796fea9a86e9eb38f.png


如何避免fail-fast抛异常


  1. 如果非要在遍历的时候修改集合,那么建议用迭代器的remove等方法,而不是用集合的remove等方法
  2. 并发的环境,那还要对Iterator对象加锁, 也可以直接使用Collections.synchronizedList
  3. CopyOnWriteArrayList(采用fail-safe)
相关文章
|
14天前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
35 5
|
22天前
|
Java 程序员
深入理解Java异常处理机制
Java的异常处理是编程中的一块基石,它不仅保障了代码的健壮性,还提升了程序的可读性和可维护性。本文将深入浅出地探讨Java异常处理的核心概念、分类、处理策略以及最佳实践,旨在帮助读者建立正确的异常处理观念,提升编程效率和质量。
104 1
|
23天前
|
Java 开发者 UED
深入探索Java中的异常处理机制##
本文将带你深入了解Java语言中的异常处理机制,包括异常的分类、异常的捕获与处理、自定义异常的创建以及最佳实践。通过具体实例和代码演示,帮助你更好地理解和运用Java中的异常处理,提高程序的健壮性和可维护性。 ##
47 2
|
23天前
|
Java 开发者
Java中的异常处理机制深度剖析####
本文深入探讨了Java语言中异常处理的重要性、核心机制及其在实际编程中的应用策略,旨在帮助开发者更有效地编写健壮的代码。通过实例分析,揭示了try-catch-finally结构的最佳实践,以及如何利用自定义异常提升程序的可读性和维护性。此外,还简要介绍了Java 7引入的多异常捕获特性,为读者提供了一个全面而实用的异常处理指南。 ####
48 2
|
26天前
|
Java 程序员 UED
深入理解Java中的异常处理机制
本文旨在揭示Java异常处理的奥秘,从基础概念到高级应用,逐步引导读者掌握如何优雅地管理程序中的错误。我们将探讨异常类型、捕获流程,以及如何在代码中有效利用try-catch语句。通过实例分析,我们将展示异常处理在提升代码质量方面的关键作用。
33 3
|
26天前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
26天前
|
运维 Java 编译器
Java 异常处理:机制、策略与最佳实践
Java异常处理是确保程序稳定运行的关键。本文介绍Java异常处理的机制,包括异常类层次结构、try-catch-finally语句的使用,并探讨常见策略及最佳实践,帮助开发者有效管理错误和异常情况。
83 4
|
26天前
|
存储 缓存 安全
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
38 4
|
25天前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
40 2
|
20天前
|
Java API 开发者
深入理解Java中的异常处理机制
本文探讨了Java编程语言中异常处理的核心概念,包括异常类型、异常捕获与抛出、以及最佳实践。通过分析常见的异常场景和处理策略,旨在帮助开发者更好地理解和运用异常处理机制,提高代码的健壮性和可维护性。文章不仅涵盖了基本的try-catch结构,还深入讨论了自定义异常的创建与使用,以及finally块的重要性和应用。此外,还将介绍一些高级技巧,如多异常捕获和嵌套异常处理,为读者提供全面的技术指导。
71 0