Java迭代foreach详解(java.util.ConcurrentModificationException的原因)
2016-02-16 19:34
501 查看
本想翻译一下java.util.ConcurrentModificationException这篇文章的。但发现讲的不够详细深入,而且最后一点还错了……查了一些资料后决定自己扩展一下。水平有限,仅仅作为一个学习总结啦。
Output:
而foreach的背后实现原理其实就是Iterator(关于Iterator可以看Java Design Pattern: Iterator),等同于注释部分代码。
在这里,迭代ArrayList的Iterator中有一个变量
此时就会抛出java.util.ConcurrentModificationException异常
过程如下图:
我们再根据源码详细的走一遍这个过程
根据代码可知,每次迭代list时,会初始化Itr的三个成员变量
接着调用
而最上面测试代码出现异常的原因在于,
接下来我们看下ArrayList的源码,了解下modCount 是如何与expectedModCount不相等的。
从上面的代码可以看出,ArrayList的add、remove、clear方法都会造成modCount的改变。迭代过程中如何调用这些方法就会造成modCount的增加,使迭代类中expectedModCount和modCount不相等。
我很任性,我就是想在迭代集合时删除集合的元素,怎么办?
细心的朋友会发现Itr中的也有一个remove方法,实质也是调用了ArrayList中的remove,但增加了
但是,这个办法的有两个弊端
1.只能进行remove操作,add、clear等Itr中没有。
2.而且只适用单线程环境。
Output:
异常的原因很简单,一个线程修改了list的modCount导致另外一个线程迭代时modCount与该迭代器的expectedModCount不相等。
此时有两个办法:
1.迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
2.采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作
CopyOnWriteArrayList是一个线程安全的ArrayList,其实现原理在于,每次add,remove等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。这样便可实现线程安全,也不需modCount域,所以对其进行remove和add不会抛出并发异常。
由于我用CopyOnWriteArrayList少,这里就不多讨论了,想了解可以看:Java并发编程:并发容器之CopyOnWriteArrayList
但是,仔细思考,还是会有几点疑惑:
1. 既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
2. ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?
我们来了解一个概念:
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
但是单线程使用不单时也可能抛出这个异常。
参考:Java提高篇(三四)—–fail-fast机制
异常产生
当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。
public class AddRemoveListElement { public static void main(String args[]) { List<String> list = new ArrayList<String>(); list.add("A"); list.add("B"); for (String s : list) { if (s.equals("B")) { list.remove(s); } } //foreach循环等效于迭代器 /*Iterator<String> iterator=list.iterator(); while(iterator.hasNext()){ String s=iterator.next(); if (s.equals("B")) { list.remove(s); } }*/ } }
Output:
异常原因
ArrayList的父类AbstarctList中有一个域modCount,每次对集合进行修改(增添元素,删除元素……)时都会
modCount++
而foreach的背后实现原理其实就是Iterator(关于Iterator可以看Java Design Pattern: Iterator),等同于注释部分代码。
在这里,迭代ArrayList的Iterator中有一个变量
expectedModCount,该变量会初始化和
modCount相等,但接下来如果集合进行修改,
modCount改变,就会造成
modCount!=expectedModCount
此时就会抛出java.util.ConcurrentModificationException异常
过程如下图:
我们再根据源码详细的走一遍这个过程
/** * AbstarctList的内部类,用于迭代 */ private class Itr implements Iterator<E> { // 将要访问的元素的索引 int cursor = 0; // 上一个访问元素的索引 int lastRet = -1; // expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量) int expectedModCount = modCount; // 判断是否还有下一个元素 public boolean hasNext() { return cursor != size(); } // 取出下一个元素 public E next() { checkForComodification(); // 关键的一行代码,判断expectedModCount和modCount是否相等 try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet == -1) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
根据代码可知,每次迭代list时,会初始化Itr的三个成员变量
int cursor = 0; //将要访问的元素的索引 int lastRet = -1; //上一个访问元素的索引 int expectedModCount = modCount; //预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)
接着调用
hasNext()循环判断访问元素的下标是否到达末尾。如果没有,调用
next()方法,取出元素。
而最上面测试代码出现异常的原因在于,
next()方法调用
checkForComodification()时,发现了
modCount != expectedModCount
接下来我们看下ArrayList的源码,了解下modCount 是如何与expectedModCount不相等的。
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方法都会造成modCount的改变。迭代过程中如何调用这些方法就会造成modCount的增加,使迭代类中expectedModCount和modCount不相等。
异常的解决
1. 单线程环境
好,现在我们已经基本了解了异常的发送原因了。接下来我们来解决它。我很任性,我就是想在迭代集合时删除集合的元素,怎么办?
Iterator<String> iter = list.iterator(); while(iter.hasNext()){ String str = iter.next(); if( str.equals("B") ) { iter.remove(); } }
细心的朋友会发现Itr中的也有一个remove方法,实质也是调用了ArrayList中的remove,但增加了
expectedModCount = modCount;保证了不会抛出java.util.ConcurrentModificationException异常。
但是,这个办法的有两个弊端
1.只能进行remove操作,add、clear等Itr中没有。
2.而且只适用单线程环境。
2. 多线程环境
在多线程环境下,我们再次试验下上面的代码public class Test2 { static List<String> list = new ArrayList<String>(); public static void main(String[] args) { list.add("a"); list.add("b"); list.add("c"); list.add("d"); new Thread() { public void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(Thread.currentThread().getName() + ":" + iterator.next()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }; }.start(); new Thread() { public synchronized void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(Thread.currentThread().getName() + ":" + element); if (element.equals("c")) { iterator.remove(); } } }; }.start(); } }
Output:
异常的原因很简单,一个线程修改了list的modCount导致另外一个线程迭代时modCount与该迭代器的expectedModCount不相等。
此时有两个办法:
1.迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
public class Test2 { static List<String> list = new ArrayList<String>(); public static void main(String[] args) { list.add("a"); list.add("b"); list.add("c"); list.add("d"); new Thread() { public void run() { Iterator<String> iterator = list.iterator(); synchronized (list) { while (iterator.hasNext()) { System.out.println(Thread.currentThread().getName() + ":" + iterator.next()); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }; }.start(); new Thread() { public synchronized void run() { Iterator<String> iterator = list.iterator(); synchronized (list) { while (iterator.hasNext()) { String element = iterator.next(); System.out.println(Thread.currentThread().getName() + ":" + element); if (element.equals("c")) { iterator.remove(); } } } }; }.start(); } }
2.采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作
public class Test2 { static List<String> list = new CopyOnWriteArrayList<String>(); public static void main(String[] args) { list.add("a"); list.add("b"); list.add("c"); list.add("d"); new Thread() { public void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(Thread.currentThread().getName() + ":" + iterator.next()); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; }.start(); new Thread() { public synchronized void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(Thread.currentThread().getName() + ":" + element); if (element.equals("c")) { list.remove(element); } } }; }.start(); } }
CopyOnWriteArrayList是一个线程安全的ArrayList,其实现原理在于,每次add,remove等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。这样便可实现线程安全,也不需modCount域,所以对其进行remove和add不会抛出并发异常。
由于我用CopyOnWriteArrayList少,这里就不多讨论了,想了解可以看:Java并发编程:并发容器之CopyOnWriteArrayList
进一步理解异常—fail-fast机制
到这里,我们似乎已经理解完这个异常的产生缘由了。但是,仔细思考,还是会有几点疑惑:
1. 既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
2. ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?
我们来了解一个概念:
fail-fast(快速失败机制)
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
但是单线程使用不单时也可能抛出这个异常。
参考:Java提高篇(三四)—–fail-fast机制
相关文章推荐
- java实现无向图的深度优先搜索和广度优先搜索
- 在运行时使用反射分析对象
- JAVA-数据库Date格式在前台JSP页面的获取
- 【Java】深夜代码祭(2)
- java中常用算法—二分法查找
- Java中关于时区的哪些事
- JAVA进行基础的文件IO读写
- Java注解@Retention&@Inherited@Target@IntDef@Documented
- 初试spark java WordCount
- SpringMVC解决GET请求时中文乱码的问题
- struts2的jsp读取action参数
- Java api 入门教程 之 JAVA的文件操作
- 启动eclipse闪退,无法启动eclipse
- MyEclipse设置Java代码注释模板
- Quartz在Spring中动态设置cronExpression (spring设置动态定时任务)
- STS 断点有斜线不起作用的解决方法
- SpringMVC 处理客户端请求的过程
- Spring
- 关于struts2 的单例和多例及线程安全的问题
- java正则表达式特殊字符