一、认识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(10, paramInt); 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事件。类似于我们在学习操作系统的时候出现的问题。