Java集合中的fail-fast(快速失败)机制详解

简介

我们知道Java中Collection接口下的很多集合都是线程不安全的, 比如 java.util.ArrayList不是线程安全的, 因此如果在使用迭代器的过程中有其他线程修改了list,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对ArrayList 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 list
注意到 modCount 声明为 volatile,保证线程之间修改的可见性。

modCount和expectedModCount

modCount和expectedModCount是用于表示修改次数的,其中modCount表示集合的修改次数,这其中包括了调用集合本身的add, remove, clear方法等修改方法时进行的修改和调用集合迭代器的修改方法进行的修改。而expectedModCount则是表示迭代器对集合进行修改的次数。

设置expectedModCount的目的就是要保证在使用迭代器期间,list对象只能有这一个迭代器对list进行修改。

在创建迭代器的时候会把对象的modCount的值传递给迭代器的expectedModCount:

 private class ListItr implements ListIterator<E> {
  private Node<E> lastReturned;
  private Node<E> next;
  private int nextIndex;
  private int expectedModCount = modCount;

如果创建多个迭代器对一个集合对象进行修改的话,那么就会有一个modCount和多个expectedModCount,且modCount的值之间也会不一样,这就导致了moCount和expectedModCount的值不一致,从而产生异常:

public E next() {
  checkForComodification();
  if (!hasNext())
  throw new NoSuchElementException();

  lastReturned = next;
  next = next.next;
  nextIndex++;
  return lastReturned.item;
 }

上面的代码中的checkForComodification会检查modCount和expectedModCount的值是否一致,不一致则抛出异常。

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

modCount是如何被修改的

 // 添加元素到队列最后
 public boolean add(E e) {
  // 修改modCount
  ensureCapacity(size + 1); // Increments modCount!!
  elementData[size++] = e;
  return true;
 }

 // 添加元素到指定的位置
 public void add(int index, E element) {
  if (index > size || index < 0)
   throw new IndexOutOfBoundsException(
   "Index: "+index+", Size: "+size);

  // 修改modCount
  ensureCapacity(size+1); // Increments modCount!!
  System.arraycopy(elementData, index, elementData, index + 1,
    size - index);
  elementData[index] = element;
  size++;
 }

 // 添加集合
 public boolean addAll(Collection<? extends E> c) {
  Object[] a = c.toArray();
  int numNew = a.length;
  // 修改modCount
  ensureCapacity(size + numNew); // Increments modCount
  System.arraycopy(a, 0, elementData, size, numNew);
  size += numNew;
  return numNew != 0;
 }

 // 删除指定位置的元素
 public E remove(int index) {
  RangeCheck(index);

  // 修改modCount
  modCount++;
  E oldValue = (E) elementData[index];

  int numMoved = size - index - 1;
  if (numMoved > 0)
   System.arraycopy(elementData, index+1, elementData, index, numMoved);
  elementData[--size] = null; // Let gc do its work

  return oldValue;
 }

 // 快速删除指定位置的元素
 private void fastRemove(int index) {

  // 修改modCount
  modCount++;
  int numMoved = size - index - 1;
  if (numMoved > 0)
   System.arraycopy(elementData, index+1, elementData, index,
        numMoved);
  elementData[--size] = null; // Let gc do its work
 }

 // 清空集合
 public void clear() {
  // 修改modCount
  modCount++;

  // Let gc do its work
  for (int i = 0; i < size; i++)
   elementData[i] = null;

  size = 0;
 }

也就是在对集合进行数据的增删的时候都会执行modcount++, 那么如果一个线程还在使用迭代器遍历这个list的时候就会发现异常, 发生 fail-fast(快速失败)

fail-fast(快速失败)和fail-safe(安全失败)比较

Iterator的快速失败是基于对底层集合做拷贝是浅拷贝,因此,它受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的

而java.util.concurrent包下面的所有的类都是使用锁实现安全失败的。

快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

fail-fast解决什么问题

fail-fast机制,是一种错误检测机制。

它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。只是在多线程环境下告诉客户端发生了多线程安全问题.
所以若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。

如何解决fail-fast事件

ArrayList对应的CopyOnWriteArrayList进行说明。我们先看看CopyOnWriteArrayList的源码:

public class CopyOnWriteArrayList<E>
 implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

 ...

 // 返回集合对应的迭代器
 public Iterator<E> iterator() {
  return new COWIterator<E>(getArray(), 0);
 }

 ...

 private static class COWIterator<E> implements ListIterator<E> {
  private final Object[] snapshot;

  private int cursor;

  private COWIterator(Object[] elements, int initialCursor) {
   cursor = initialCursor;
   // 新建COWIterator时,将集合中的元素保存到一个新的拷贝数组中。
   // 这样,当原始集合的数据改变,拷贝数据中的值也不会变化。
   snapshot = elements;
  }

  public boolean hasNext() {
   return cursor < snapshot.length;
  }

CopyOnWriteArrayList是自己实现Iterator, 并且CopyOnWriteArrayList的Iterator实现类中,没有所谓的checkForComodification(),更不会抛出ConcurrentModificationException异常

CopyOnWriteArrayList在进行新建COWIterator时,将集合中的元素保存到一个新的拷贝数组中。这样,当原始集合的数据改变,拷贝数据中的值也不会变化。

总结

到此这篇关于Java集合中的fail-fast(快速失败)机制的文章就介绍到这了,更多相关Java集合fail-fast(快速失败)机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java你告诉我 fail-fast 是什么鬼

    01.前言 说起来真特么惭愧:十年 IT 老兵,Java 菜鸟一枚.今天我才了解到 Java 还有 fail-fast 一说.不得不感慨啊,学习真的是没有止境.只要肯学,就会有巨多巨多别人眼中的"旧"知识涌现出来,并且在我这全是新的. 能怎么办呢?除了羞愧,就只能赶紧全身心地投入学习,把这些知识掌握. 为了镇楼,必须搬一段英文来解释一下 fail-fast. In systems design, a fail-fast system is one which immediately r

  • 由ArrayList来深入理解Java中的fail-fast机制

    1. fail-fast简介 "快速失败"也就是fail-fast,它是Java集合的一种错误检测机制.某个线程在对collection进行迭代时,不允许其他线程对该collection进行结构上的修改. 例如:假设存在两个线程(线程1.线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生f

  • 老生常谈java中的fail-fast机制

    在JDK的Collection中我们时常会看到类似于这样的话: 例如,ArrayList: 注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证.快速失败迭代器会尽最大努力抛出 ConcurrentModificationException.因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug. HashMap中: 注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时

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

    fail-fast 机制是java集合(Collection)中的一种错误机制.当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件.例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了:那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件. fail-fast 机制是java集合(Collection)中的一种错误机制.当多个线程对同一个集合的内容进行操作时,

  • 一不小心就让Java开发踩坑的fail-fast是个什么鬼?(推荐)

    我在<为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作>一文中曾经介绍过Java中的fail-fast机制,但是并没有深入介绍,本文,就来深入介绍一下fail-fast. 什么是fail-fast 首先我们看下维基百科中关于fail-fast的解释: In systems design, a fail-fast system is one which immediately reports at its interface any condition that

  • 基于java集合中的一些易混淆的知识点(详解)

    (一) collection和collections 这两者均位于java.util包下,不同的是: collection是一个集合接口,有ListSet等常见的子接口,是集合框架图的第一个节点,,提供了对集合对象进行基本操作的一系列方法. 常见的方法有: boolean add(E e) 往容器中添加元素:int size() 返回collection的元素数:boolean isEmpty() 判断此容器是否为空: boolean contains(Object o) 如果此collecti

  • Java集合中的fail-fast(快速失败)机制详解

    简介 我们知道Java中Collection接口下的很多集合都是线程不安全的, 比如 java.util.ArrayList不是线程安全的, 因此如果在使用迭代器的过程中有其他线程修改了list,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略. 这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对ArrayList 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 exp

  • java编程中自动拆箱与自动装箱详解

    什么是自动装箱拆箱 基本数据类型的自动装箱(autoboxing).拆箱(unboxing)是自J2SE 5.0开始提供的功能. 一般我们要创建一个类的对象实例的时候,我们会这样: Class a = new Class(parameter); 当我们创建一个Integer对象时,却可以这样: Integer i = 100; (注意:不是 int i = 100; ) 实际上,执行上面那句代码的时候,系统为我们执行了:Integer i = Integer.valueOf(100); (感谢@

  • Java集合操作之List接口及其实现方法详解

    在介绍List接口之前,我们先来看看 Collection 接口,因为Collection接口是 List / Set / Queue 接口的父接口,List / Set / Queue 的实现类中很多的操作方法其实还是调用Collection类定义的方法. 一.Collection接口 在Collection接口中,定义了如下的方法: 其中方法可以分为以下几类: 数据操作类方法:add/addAll/remove/removeAll/clear/retainAll/iterator 判断类方法

  • Java程序中实现调用Python脚本的方法详解

    本文实例讲述了Java程序中实现调用Python脚本的方法.分享给大家供大家参考,具体如下: 在程序开发中,有时候需要Java程序中调用相关Python脚本,以下内容记录了先关步骤和可能出现问题的解决办法. 1.在Eclipse中新建Maven工程: 2.pom.xml文件中添加如下依赖包之后update maven工程: <dependency> <groupId>org.python</groupId> <artifactId>jython</ar

  • Java链表中添加元素的原理与实现方法详解

    本文实例讲述了Java链表中添加元素的原理与实现方法.分享给大家供大家参考,具体如下: 1.链表中头节点的引入 1.1基本的链表结构: 1.2对于链表来说,若想访问链表中每个节点则需要把链表的头存起来,假如链表的头节点为head,指向链表中第一个节点,如图: 1.3使用代码表示此时的链表 //定义头节点 private Node head; //节点个数 private int size; //无参数构造函数 public LinkedList() { head = null; size = 0

  • Java多线程中的wait/notify通信模式实例详解

    前言 最近在看一些JUC下的源码,更加意识到想要学好Java多线程,基础是关键,比如想要学好ReentranLock源码,就得掌握好AQS源码,而AQS源码中又有很多Java多线程经典的一些应用:再比如看了线程池的核心源码实现,又学到了很多核心实现,其实这些都可以提出来慢慢消化并变成自己的知识点,今天这个Java等待/通知模式其实是Thread.join()实现的关键,还有线程池工作线程中线程跟线程之间的通信的核心所在,故在此为了加深理解,做此记录! 参考资料<Java并发编程艺术>(电子PD

  • JAVA并发中VOLATILE关键字的神奇之处详解

    并发编程中的三个概念: 1.原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. 2.可见性 对于可见性,Java提供了volatile关键字来保证可见性. 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值. 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保

  • JDK8中的HashMap初始化和扩容机制详解

    一.HashMap初始化方法 HashMap() 不带参数,默认初始化大小为16,加载因子为0.75: HashMap(int initialCapacity) 指定初始化大小: HashMap(int initialCapacity, float loadFactor) 指定初始化大小和加载因子大小: HashMap(Map<? extends K,? extends V> m) 用现有的一个map来构造HashMap. 二.分析初始化过程 1.初始化代码测试用例 Map<String

  • Java 8 中的 10 个特性总结及详解

    你以前听到的谈论关于Java8的所有都是围绕lambda表达式. 但它仅仅是Java8的一部分. Java 8 有许多新特性-一些强大的新类和语法, 还有其他的从一开始就应该具有的东西. 我将要介绍我认为值得了解的10个精华特性. 它们中最少也会有一个或两个你想要试一试, 所以我们开始吧! 1. 默认方法 Java语言一个新添加的特性是你可以为接口(interface)的方法添加方法体(称为默认方法). 这些方法会被隐式地添加到实现这个接口的类中. 这能使你在不破坏代码的情况下为已存在的库添加新

随机推荐