今天来看java集合中一个常见的错误机制fail-fast机制。出现在这个错误机制的本质就是因为单线程和多线程的不同。下面就好好看一下这个机制是怎么是出现的。

一、认识fail-fast

今天在运行项目的时候,突然就出现了ConcurrentModificationException异常。原因是多线程中使用的,因为在多线程中使用了ArrayList,造成了这么一个异常。这是今天所讲的集合的fai-fast机制。

我们先来看看维基百科中的解释:

在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。

   概念:fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。

二、分析fail-fast

  为了更好的去了解一下fail-fast,我们先去实现一下这个错误如何产生。

异常出现

下面通过一个示例来展示

public class Test {
   private static List<String> list = new ArrayList<String>();
   public static void main(String[] args) {
       //两个线程对同一个ArrayList进行操作
       new ThreadOne().start();
       new ThreadTwo().start();
  }
   private static void printAll() {
       String value = null;
       Iterator iter = list.iterator();
       while(iter.hasNext()) {
           value = (String)iter.next();
           System.out.println(value);
      }
  }
   //线程一:向list中依次添加数据,然后printAll()整个list
   private static class ThreadOne extends Thread {
       public void run() {
           for (int i=0;i<6;i++) {
               list.add(String.valueOf("线程一:java的架构师技术栈"+i));
               printAll();
          }
      }
  }
//线程二:对ArrayList实现同样的操作
   private static class ThreadTwo extends Thread {
       public void run() {
           for (int i=0;i<6;i++) {
               list.add(String.valueOf("线程二:java的架构师技术栈"+i));
               printAll();
          }
      }
  }
}

   看一下在Eclipse中处理的结果:

图片

现在我们fail-fast实现的方式我们都已经知道了,fail-fast快速失败是在迭代的时候产生的,但是是如何产生的呢?下面我们再来深入的分析一下:

根本原因:

从前面我们知道fail-fast是在操作迭代器时产生的。现在我们来看看ArrayList中迭代器的源代码:

private class IteratorTest implements Iterator<E> {
       int cursor;
       int lastRet = -1;
       int expectedModCount = ArrayList.this.modCount;
       public boolean hasNext() {
           return (this.cursor != ArrayList.this.size);
      }
       public E next() {
           checkForComodification();
      }
       public void remove() {
           if (this.lastRet < 0)
               throw new IllegalStateException();
           checkForComodification();
      }
       final void checkForComodification() {
           if (ArrayList.this.modCount == this.expectedModCount)
               return;
           throw new ConcurrentModificationException();
      }
}

从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。到了这一步我们也知道了,想要弄清楚fail-fast机制,首先我们需要搞清楚modCount 和expectedModCount。

       expectedModCount 是在IteratorTest中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:

protected transient int modCount = 0;

   那么他什么时候因为什么原因而发生改变呢?请看ArrayList的源码:

   public boolean add(E paramE) {
       ensureCapacityInternal(this.size + 1);
  }

   private void ensureCapacityInternal(int paramInt) {
       if (this.elementData == EMPTY_ELEMENTDATA)
           paramInt = Math.max(10paramInt);
       ensureExplicitCapacity(paramInt);
  }
   //看到没,是在这里面实现的,修改了modCount
   private void ensureExplicitCapacity(int paramInt) {
       this.modCount += 1;    //修改modCount
  }
  //同理,其他的几个也实现了对modCount的修改
  //remove、
  //fastRemove
  //clear

   从上面的源代码我们可以看出,只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 与modCount的改变不同步,导致两者之间不等,从而产生fail-fast机制。

我想各位已经基本了解了fail-fast的机制,那么平常我们如何去规避这种情况呢?这里有两种解决方案:

       方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList(不推荐)

       方案二:使用CopyOnWriteArrayList来替换ArrayList。

       CopyOnWriteArrayList为什么能解决这个问题呢?CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。但是。CopyOnWriteArrayList中的读方法是没有加锁的。

我们只需要记住一句话,那就是CopyOnWriteArrayList是线程安全的,所以我们在多线程的环境下面需要去使用这个就可以了。关于CopyOnWriteArrayList更加深入的用法,会在以后的章节中去解释说明。

三、总结

现在我们对fail-fast机制都已经有了了解了。其出现的原因是:当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。类似于我们在学习操作系统的时候出现的问题。  


更多相关文章

  1. jvm系列(3)类加载机制
  2. jvm系列(4)垃圾回收机制
  3. 序列化系列(1)java序列化机制
  4. 成为 Java GC 专家( 3 ): 如何优化 Java 垃圾回收机制
  5. 成为 Java GC 专家( 2 ):如何监控 Java 垃圾回收机制
  6. 多线程环境下生成随机数
  7. 线程池调整真的很重要
  8. Java线程之线程的调度-休眠
  9. Java线程之线程的调度-优先级

随机推荐

  1. mysql全文搜索为空
  2. Spark踩坑记——数据库(Hbase+Mysql)转
  3. 10个MySQL数据库备份教程推荐
  4. SQL:使用IN子句搜索列值
  5. 使用SQL-DMO实现定制SQL Scripts
  6. SQL 关于row_number()over()
  7. oracle新接触的sql语句
  8. Mysql完全手册(笔记二,使用数据与性能优化)
  9. 如何构建将被输入下拉框的数据?
  10. 使用navicat for mysql 将查询语句过滤的