今天处理一个多线程业务,这个业务原来是在单线程中运行的,如今对这个业务进行改造,使其能在多线程中运行以加快处理效率。结果抛出了ConcurrentModificationException异常。究其缘由,原来此业务使用了ArrayList来存储数据。而ArrayList集合是不支持在多线程中使用的,由于在多线程中,一个线程经过iterator去遍历某集合的过程当中,若该集合内容被其余线程所改变,那么(遍历线程的集合)就会抛出ConcurrentModificationException异常。这是集合的fai-fast机制(具体原理请见后面内容)java
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操做时,就可能会产生fail-fast事件。例如:当某一个线程A经过iterator去遍历某集合的过程当中,若该集合的内容被其余线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。为何说是有可能会产生fail-fast事件?由于迭代器的快速失败行为没法获得保证,由于通常来讲,不可能对是否出现不一样步并发修改作出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。所以,为提升这类迭代器的正确性而编写一个依赖于此异常的程序是错误的作法:迭代器的快速失败行为应该仅用于检测 bug。数组
下面经过一个示例来展现Fail-Fast安全
import java.util.*; import java.util.concurrent.*; /** * java集合中Fast-Fail的测试程序。 * * fast-fail事件产生的条件:当多个线程对Collection进行操做时,若其中某一个线程经过iterator去遍历集合时,该集合的内容被其余线程所改变;则会抛出ConcurrentModificationException异常。 * fast-fail解决办法:经过util.concurrent集合包下的相应类去处理,则不会产生fast-fail事件。 * * 本例中,分别测试ArrayList和CopyOnWriteArrayList这两种状况。ArrayList会产生fast-fail事件,而CopyOnWriteArrayList不会产生fast-fail事件。 * 使用ArrayList时,会产生fast-fail事件,抛出ConcurrentModificationException异常;定义以下: * private static List<String> list = new ArrayList<String>(); * 使用时CopyOnWriteArrayList,不会产生fast-fail事件;定义以下: * private static List<String> list = new CopyOnWriteArrayList<String>(); * * @author kucs */ public class FastFailTest { private static List<String> list = new ArrayList<String>(); //private static List<String> list = new CopyOnWriteArrayList<String>(); public static void main(String[] args) { // 同时启动两个线程对list进行操做! new ThreadOne().start(); new ThreadTwo().start(); } private static void printAll() { System.out.println(""); String value = null; Iterator iter = list.iterator(); while(iter.hasNext()) { value = (String)iter.next(); System.out.print(value+", "); } } /** * 向list中依次添加0,1,2,3,4,5,每添加一个数以后,就经过printAll()遍历整个list */ private static class ThreadOne extends Thread { public void run() { int i = 0; while (i<6) { list.add(String.valueOf(i)); printAll(); i++; } } } /** * 向list中依次添加10,11,12,13,14,15,每添加一个数以后,就经过printAll()遍历整个list */ private static class ThreadTwo extends Thread { public void run() { int i = 10; while (i<16) { list.add(String.valueOf(i)); printAll(); i++; } } } }
运行结果:数据结构
0, 10, 0, 10, 1, 0, 10, 1, 2, 0, 0, 10, 1, 2, 3, 0, 10, 1, 2, 3, 4, 0, 10, 1, 2, 3, 4, 5, Exception in thread "Thread-1" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(Unknown Source) at java.util.ArrayList$Itr.next(Unknown Source) at FastFailTest.printAll(FastFailTest.java:35) at FastFailTest.access$300(FastFailTest.java:18) at FastFailTest$ThreadTwo.run(FastFailTest.java:62)
(如下内容参照博客:http://blog.csdn.net/chenssy/article/details/38151189)多线程
经过上面的示例和讲解,我初步知道fail-fast产生的缘由就在于程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其作了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast。并发
要了解fail-fast机制,咱们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不容许这种修改时就抛出该异常。同时须要注意的是,该异常不会始终指出对象已经由不一样线程并发修改,若是单线程违反了规则,一样也有可能会抛出改异常。源码分析
诚然,迭代器的快速失败行为没法获得保证,它不能保证必定会出现该错误,可是快速失败操做会尽最大努力抛出ConcurrentModificationException异常,因此所以,为提升此类操做的正确性而编写一个依赖于此异常的程序是错误的作法,正确作法是:ConcurrentModificationException 应该仅用于检测 bug。下面我将以ArrayList为例进一步分析fail-fast产生的缘由。测试
从前面咱们知道fail-fast是在操做迭代器时产生的。如今咱们来看看ArrayList中迭代器的源代码:this
private class Itr 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 ,他们的值在何时发生改变的。spa
expectedModCount 是在Itr中定义的: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); } private void ensureExplicitCapacity(int paramInt) { this.modCount += 1; //修改modCount /** 省略此处代码 */ } public boolean remove(Object paramObject) { int i; if (paramObject == null) for (i = 0; i < this.size; ++i) { if (this.elementData[i] != null) continue; fastRemove(i); return true; } else for (i = 0; i < this.size; ++i) { if (!(paramObject.equals(this.elementData[i]))) continue; fastRemove(i); return true; } return false; } private void fastRemove(int paramInt) { this.modCount += 1; //修改modCount /** 省略此处代码 */ } public void clear() { this.modCount += 1; //修改modCount /** 省略此处代码 */ }
从上面的源代码咱们能够看出,ArrayList中不管add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会致使modCount的改变。因此咱们这里能够初步判断因为expectedModCount 得值与modCount的改变不一样步,致使二者之间不等从而产生fail-fast机制。知道产生fail-fast产生的根本缘由了,咱们能够有以下场景:
有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增长一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount = N ,而modCount = N + 1,二者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。
因此,直到这里咱们已经彻底了解了fail-fast产生的根本缘由了。知道了缘由就好找解决办法了。
经过前面的实例、源码分析,我想各位已经基本了解了fail-fast的机制,下面我就产生的缘由提出解决方案。这里有两种解决方案:
方案一:在遍历过程当中全部涉及到改变modCount值得地方所有加上synchronized或者直接使用Collections.synchronizedList,这样就能够解决。可是不推荐,由于增删形成的同步锁可能会阻塞遍历操做。
方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。
CopyOnWriteArrayList为什么物?ArrayList 的一个线程安全的变体,其中全部可变操做(add、set 等等)都是经过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,可是在两种状况下,它很是适合使用。1:在不能或不想进行同步遍历,但又须要从并发线程中排除冲突时。2:当遍历操做的数量大大超过可变操做的数量时。遇到这两种状况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。那么为何CopyOnWriterArrayList能够替代ArrayList呢?
第1、CopyOnWriterArrayList的不管是从数据结构、定义都和ArrayList同样。它和ArrayList同样,一样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。
第2、CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器彻底不会产生fail-fast机制。请看:
private static class COWIterator<E> implements ListIterator<E> { /** 省略此处代码 */ public E next() { if (!(hasNext())) throw new NoSuchElementException(); return this.snapshot[(this.cursor++)]; } /** 省略此处代码 */ }
CopyOnWriterArrayList的方法根本就没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等。它为何会这么作,凭什么能够这么作呢?咱们以add方法为例:
public boolean add(E paramE) { ReentrantLock localReentrantLock = this.lock; localReentrantLock.lock(); try { Object[] arrayOfObject1 = getArray(); int i = arrayOfObject1.length; Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1); arrayOfObject2[i] = paramE; setArray(arrayOfObject2); int j = 1; return j; } finally { localReentrantLock.unlock(); } } final void setArray(Object[] paramArrayOfObject) { this.array = paramArrayOfObject; }
CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不一样点就在于,下面三句代码:
Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1); arrayOfObject2[i] = paramE; setArray(arrayOfObject2);
就是这三句代码使得CopyOnWriterArrayList不会抛ConcurrentModificationException异常。他们所展示的魅力就在于copy原来的array,再在copy数组上进行add操做,这样作就彻底不会影响COWIterator中的array了。
因此CopyOnWriterArrayList所表明的核心概念就是:任何对array在结构上有所改变的操做(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成以后改变原有数据的引用便可。同时这样形成的代价就是产生大量的对象,同时数组的copy也是至关有损耗的。