在JDK的Collection中我们时常会看到类似于这样的话:
例如,ArrayList:
注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。 因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。
HashMap中:
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
在这两段话中反复地提到”快速失败”。那么何为”快速失败”机制呢?
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
1、fail-fast示例
public class FailFastDemoOne { /** * 初始化list * @return */ public List<String> getListStr(){ ArrayList<String> list = new ArrayList<String>(); list.add("test 1"); list.add("test 2"); list.add("test 3"); list.add("test 4"); return list; } /** * 这种不使用增强的for循环的也可以正常删除和遍历, * 这里所谓的正常是指它不会报异常,但是删除后得到的 * 数据不一定是正确的,这主要是因为删除元素后,被删除元素后 * 的元素索引发生了变化。假设被遍历list中共有10个元素,当 * 删除了第3个元素后,第4个元素就变成了第3个元素了,第5个就变成 * 了第4个了,但是程序下一步循环到的索引是第4个, * 这时候取到的就是原本的第5个元素了。 */ @Test public void listRemoveByFor(){ List<String> list = this.getListStr(); for(int i=0; i<list.size(); i++){ String str = list.get(i); if(list.get(1).equals(str)){ list.remove(str); } System.out.println("1"+str); } } /** * 使用增强的for循环 * 在循环过程中从List中删除非基本数据类型以后,继续循环List时会报ConcurrentModificationException(增强for循环底层还是迭代方式实现的) */ @Test public void listRemoveByForeach(){ List<String> list = this.getListStr(); for(String str : list){ if(list.get(1).equals(str)){ list.remove(str); } System.out.println(str); } } /** * 使用增强的for循环 * 在循环过程中从List中删除非基本数据类型以后,立即跳出,不会出现异常 */ @Test public void listRemoveBreakByForeach(){ List<String> list = this.getListStr(); for(String str : list){ if(list.get(1).equals(str)){ list.remove(str); break; } System.out.println(str); } } /** * 使用Iterator的方式可以顺利删除和遍历 */ @Test public void iteratorRemove() { List<String> list = this.getListStr(); System.out.println(list); Iterator<String> strIter = list.iterator(); while (strIter.hasNext()) { String str = strIter.next(); if (str.contains("2")){ strIter.remove();//这里要使用Iterator的remove方法移除当前对象,如果使用List的remove方法,则同样会出现ConcurrentModificationException } System.out.println(str); } System.out.println(list); } /** * 使用Iterator的方式可以顺利删除和遍历 */ @Test public void iteratorListRemove() { List<String> list = this.getListStr(); System.out.println(list); Iterator<String> strIter = list.iterator(); while (strIter.hasNext()) { String str = strIter.next(); if (str.contains("2")){ list.remove(1);//使用List的remove方法,同样会出现ConcurrentModificationException, // strIter.remove();//这里要使用Iterator的remove方法移除当前对象 } System.out.println(str); } System.out.println(list); } }
public class FailFastDemoTwo { private static List<Integer> list = new ArrayList<>(); /** * @desc:线程one迭代list */ private static class threadOne extends Thread { public void run() { Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { int i = iterator.next(); System.out.println("ThreadOne 遍历:" + i); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * @desc:当i == 3时,修改list */ private static class threadTwo extends Thread { public void run() { int i = 0; while (i < 6) { System.out.println("ThreadTwo run:" + i); if (i == 3) { list.remove(i); } i++; } } } public static void main(String[] args) { for (int i = 0; i < 10; i++) { list.add(i); } new threadOne().start(); new threadTwo().start(); } }
运行结果:
ThreadOne 遍历:0 ThreadTwo run:0 ThreadTwo run:1 ThreadTwo run:2 ThreadTwo run:3 ThreadTwo run:4 ThreadTwo run:5 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at test12.FailFastDemoTwo$threadOne.run(FailFastDemoTwo.java:17)
2、fail-fast产生原因
通过上面的示例和讲解,我初步知道fail-fast产生的原因就在于程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast。
要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出改异常。
诚然,迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常,所以因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。
当我们调用ArrayList类的 iterator() 方法时,返回的是Itr 对象,这个类是 ArrayList 的内部类。以下是该内部类的源码.
private class Itr implements Iterator<E> { int cursor; //游标, 下一个要返回的元素的索引 int lastRet = -1; // 返回最后一个元素的索引; 如果没有这样的话返回-1. int expectedModCount = modCount; //通过 cursor != size 判断是否还有下一个元素 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];//返回索引为i处的元素,并将 lastRet赋值为i } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet);//调用ArrayList的remove方法删除元素 cursor = lastRet;//游标指向删除元素的位置,本来是lastRet+1的,这里删除一个元素,然后游标就不变了 lastRet = -1;//lastRet恢复默认值-1 expectedModCount = modCount;//expectedModCount值和modCount同步,因为进行add和remove操作,modCount会加1 } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) {//便于进行forEach循环 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(); } //前面在新增元素add() 和 删除元素 remove() 时,我们可以看到 modCount++。修改set() 是没有的 //也就是说不能在迭代器进行元素迭代时进行增加和删除操作,否则抛出异常 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。所以要弄清楚为什么会产生fail-fast机制我们就必须要用弄明白为什么modCount != expectedModCount ,他们的值在什么时候发生改变的。
expectedModCount 是在Itr中定义的:int expectedModCount = modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:
protected transient int modCount = 0;
在分析ArrayList源码时我们了解到,当我们调用add、remove、clear、trimToSize等方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变,但是expectedModCount并没有改变。所以由于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产生的根本原因了。知道了原因就好找解决办法了
3、fail-fast解决办法
方案一、
在对arraylist修改modcount的操作时加锁,同时在整个迭代器操作时对arraylist对象加锁,这样保证在迭代器执行期间,modcount无法被改变;或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
方案二、
使用CopyOnWriteArrayList来替换ArrayList。
方案三、
尽量使用局部变量,这样根本上解决线程安全问题,同时在注意下同一线程时迭代器过程中不要对list做修改modcount操作即可
参考:
还没有评论,来说两句吧...