【1】ConcurrentModificationException 异常解析和快速失败,安全失败
目录
一、引起异常的代码
二、foreach原理
三、从ArrayList源码找原因
四、单线程解决方案
五、在多线程环境下的解决方法
一、引起异常的代码
以下三种的遍历集合对象时候,执行集合的remove和add的操作时候都会引起java.util.ConcurrentModificationException异常。
注:set方法不会导致该异常,看了源码set没有改变modcount。快速失败迭代器在遍历时不允许结构性修改,javadoc中对此的解释是“结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。”
public class Test { public static void main(String[] args) { List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("b");
list.add("b"); // foreach循环
for (String str : list) {
if (str.equals("b")) {
list.remove(str);
}
} // for循环借助迭代器遍历Collection对象
for (Iterator<String> it = list.iterator(); it.hasNext();) {
String value = it.next();
if (value.equals("b")) {
list.remove(value);
}
} // 迭代器遍历
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String value = it.next();
if (value.equals("3")) {
list.remove(value);
}
}
System.out.println(list);
}
}
抛出的异常:
从异常信息可以发现,异常出现在checkForComodification()方法中。不忙看checkForComodification()方法的具体实现,先根据程序的代码一步一步看ArrayList源码的实现。
二、foreach原理
(直接看结论即可)
首先、追究foreach的原理,暂时删除其他的遍历方法,只保留foreach的写法:
public class Test { public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("b");
list.add("b");
// foreach循环
for (String str : list) {
if (str.equals("b")) {
list.remove(str);
}
}
}
}
编译后的.class文件(eclipse 直接打开可以查看),截取其中for循环的部分:
44 aload_1 [list]
45 invokeinterface java.util.List.iterator() : java.util.Iterator [29] [nargs: 1]
50 astore_3
51 goto 81
54 aload_3
55 invokeinterface java.util.Iterator.next() : java.lang.Object [33] [nargs: 1]
60 checkcast java.lang.String [39]
63 astore_2 [str]
64 aload_2 [str]
65 ldc <String "b"> [27]
67 invokevirtual java.lang.String.equals(java.lang.Object) : boolean [41]
70 ifeq 81
73 aload_1 [list]
74 aload_2 [str]
75 invokeinterface java.util.List.remove(java.lang.Object) : boolean [44] [nargs: 2]
80 pop
81 aload_3
82 invokeinterface java.util.Iterator.hasNext() : boolean [47] [nargs: 1]
87 ifne 54
第45行:调用List中的list.iterator()方法,获取集合的迭代器Iterator对象。
第51行:注意,goto 81,因此是调用第81、82行的hasNext()方法。
第55行:调用next方法,获取第一个list中第一个元素:String字符串。
第67行:调用String的equals方法比较。
第75行:注意,此时remove方法仍然是list的方法,而不是迭代器的remove。
第82行:调用迭代器的hasNext()方法,判断是否继续遍历。
经过整理、优化,foreach的底层代码可以使用下方的代码替换:
public void test1() {
ArrayList<String> list = new ArrayList<String>();
list.add("b");
list.add("b");
list.add("b");
Iterator<String> iterator = list.iterator();//获取迭代器
while (iterator.hasNext()) {//继续循环
String value = iterator.next();//获取遍历到的值
if (value.equals("b")) {
list.remove(value);//list的remove
}
}
}
结论:
1、遍历集合的增强for循环最终都是使用的Iterator迭代器。
2、集合的remove(add)方法却仍然调用list的方法,而不是Iterator的方法。
不使用迭代器和不使用增强for循环是不会引起ConcurrentModificationException的,参看单线程解决方案3.不使用Iterator进行遍历(即使用for ( int i = 0; i < myList.size(); i++)形式)
三、从ArrayList源码找原因
跟进ArrayList的源码看, 搜索iterator()方法看其获得的迭代器, 发现没有! 于是追其父类 AbstractList, iterator()方法返回new Itr()!
查看Itr中的两个重要的方法: hasNext与next
public boolean hasNext() {
return cursor != size();
} public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
看next中调用的checkForComodification(), 在remove方法中也调用了checkForComodification()!接着checkForComodification()方法里面在做些什么事情!
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
所以在迭代的过程中,hasNext()是不会抛出ConcurrentModificationException的, next和remove可能方法会抛! 抛异常的标准就是modCount != expectedModCount!继续跟踪这两个变量,在Itr类的成员变量里对expectedModCount初始化的赋值是int expectedModCount = modCount;
那么这个modCount呢.? 这个是AbstractList中的一个protected的变量, 在对集合增删的操作中均对modCount做了修改, 因为这里是拿ArrayList为例, 所以直接看ArrayList中有没有覆盖父类的add? 结果发现覆盖了
public boolean add(E e) {
ensureCapacity(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
} public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
remove方法中也做了modCount++,
ArrayList中的remove做的事情:
public boolean remove(Object paramObject) {
int i;
if (paramObject == null) {
for (i = 0; i < size; i++) {
if (elementData[i] == null) {
fastRemove(i);
return true;
}
}
} else {
for (i = 0; i < size; i++) {
if (paramObject.equals(elementData[i])) {
fastRemove(i);
return true;
}
}
}
return false;
} private void fastRemove(int paramInt) {
modCount += 1;
int i = size - paramInt - 1;
if (i > 0) {
System.arraycopy(elementData, paramInt + 1, elementData, paramInt, i);
}
elementData[(--size)] = null;
}
当我获得迭代器之前, 无论对集合做了多少次添加删除操作, 都没有关系, 因为对expectedModCount赋值是在获取迭代器的时候初始化的.
关键点就在于:调用list.remove()或list.add()方法导致modCount和expectedModCount的值不一致。
四、单线程解决方案
1、对于没有使用foreach循环,代码里使用了迭代器的程序,可以把list.remove(value);替换为:iterator.remove();
看下 iterator.remove();的具体实现:
public void remove() {
if (lastRet < 0) {
throw new IllegalStateException();
}
checkForComodification();
try {
remove(lastRet);
if (lastRet < cursor) {
cursor -= 1;
}
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException localIndexOutOfBoundsException) {
throw new ConcurrentModificationException();
}
}
iterator.remove();相比list.remove(value);多了一步expectedModCount = modCount; 此时保证了checkForComodification()方法检查通过。
代码改成如下所示:
public void test1() {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("b");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String value = it.next();
if (value.equals("b")) {
// list.remove(value);
it.remove();
}
}
}
2、使用临时的集合,把需要remove的元素保存在临时的集合中,最后再把临时集合一起remove掉。
public void test2() {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("b");
// 临时的list_add
List<String> list_add = new ArrayList<String>();
for (String str : list) {
if (str.equals("b")) {
list_add.add(str);
}
}
list.removeAll(list_add);//最后统一移除
System.out.println(list);
}
3.不使用Iterator进行遍历,即使用for ( int i = 0; i < myList.size(); i++)形式。需要注意的是自己保证索引正常
for ( int i = 0; i < myList.size(); i++) {
String value = myList.get(i);
System. out.println( "List Value:" + value);
if (value.equals( "3")) {
myList.remove(value); // ok
i--; // 因为位置发生改变,所以必须修改i的位置
}
}
System. out.println( "List Value:" + myList.toString());
五、在多线程环境下的解决方法
下面的例子中开启两个子线程,一个进行遍历,另外一个有条件删除元素:
final List myList = createTestData(); new Thread(new Runnable() { @Override
public void run() {
for (String string : myList) {
System.out.println("遍历集合 value = " + string); try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start(); new Thread(new Runnable() { @Override
public void run() { for (Iterator it = myList.iterator(); it.hasNext();) {
String value = it.next(); System.out.println("删除元素 value = " + value); if (value.equals( "3")) {
it.remove();
} try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Exception in thread "Thread-0" 删除元素 value = 4
java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
at java.util.AbstractList$Itr.next(Unknown Source)
at list.ConcurrentModificationExceptionStudy$1.run(ConcurrentModificationExceptionStudy.java:42)
at java.lang.Thread.run(Unknown Source)
删除元素 value = 5
有可能有朋友说ArrayList是非线程安全的容器,换成Vector就没问题了,实际上换成Vector还是会出现这种错误。
原因在于,虽然Vector的方法采用了synchronized进行了同步,但是实际上通过Iterator访问的情况下,每个线程里面返回的是不同的iterator,也即是说expectedModCount是每个线程私有。假若此时有2个线程,线程1在进行遍历,线程2在进行修改,那么很有可能导致线程2修改后导致Vector中的modCount自增了,线程2的expectedModCount也自增了,但是线程1的expectedModCount没有自增,此时线程1遍历时就会出现expectedModCount不等于modCount的情况了。
结论:
上面的例子在多线程情况下,使用it.remove(),
说明使用it.remove()的办法在同一个线程执行的时候是没问题的,但是在多线程进行迭代情况下依然可能出现异常。
参看iterator.remove();的具体实现,如果在iterator.remove()的expectedModCount = modCount;之前线程切换,则另一个线程检查expectedModCount和modCount不一致,抛ConcurrentModificationException异常
解决方案 :
1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;
2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。
CopyOnWriteArrayList注意事项
(1) CopyOnWriteArrayList不能使用Iterator.remove()进行删除。
(2) CopyOnWriteArrayList使用Iterator且使用List.remove(Object);会出现如下异常:
java.lang.UnsupportedOperationException: Unsupported operation remove
at java.util.concurrent.CopyOnWriteArrayList$ListIteratorImpl.remove(CopyOnWriteArrayList.java:804)
原因是CopyOnWriteArrayList的阉割版迭代器COWIterator源码中
static final class COWIterator<E> implements ListIterator<E> {
public void remove() {
throw new UnsupportedOperationException();
}
//省略
}
六.Java快速失败(fail-fast)和安全失败(fail-safe)
当错误发生时,如果系统立即关闭,即是快速失败,系统不会继续运行。运行中发生错误,它会立即停止操作,错误也会立即暴露。而安全失败系统在错误发生时不会停止运行。它们隐蔽错误,继续运行,而不会暴露错误。这两种模式,孰优孰优,是系统设计中常讨论的话题,在此,我们只讨论java中的快速失败和安全失败迭代器。
Java快速失败与安全失败迭代器 :
java迭代器提供了遍历集合对象的功能,集合返回的迭代器有快速失败型的也有安全失败型的,快速失败迭代器在迭代时如果集合类被修改,立即抛出ConcurrentModificationException异常,而安全失败迭代器不会抛出异常,因为它是在集合类的克隆对象上操作的。我们来看看快速失败和 安全失败迭代器的具体细节。
java快速失败迭代器 :
大多数集合类返回的快速失败迭代器在遍历时不允许结构性修改(结构性修改指添加,删除) 当遍历的同时被结构性修改,就会抛出ConcurrentModificationException异常,而当集合是被迭代器自带的方法(如remove())修改时,不会抛出异常。
Java安全失败迭代器 :
安全失败迭代器在迭代中被修改,不会抛出任何异常,因为它是在集合的克隆对象迭代的,所以任何对原集合对象的结构性修改都会被迭代器忽略,但是这类迭代器有一些缺点,其一是它不能保证你迭代时获取的是最新数据,因为迭代器创建之后对集合的任何修改都不会在该迭代器中更新。
java.util包下的集合类都是快速失败的,java.util.concurrent包下的容器都是安全失败如ConcurrentHashMap
ConcurrentHashMap迭代器复制了一份map:
static class BaseIterator<K,V> extends Traverser<K,V> {
final ConcurrentHashMap<K,V> map;
Node<K,V> lastReturned;
BaseIterator(Node<K,V>[] tab, int size, int index, int limit,
ConcurrentHashMap<K,V> map) {
super(tab, size, index, limit);
this.map = map;
advance();
}
https://blog.csdn.net/shaohe18362202126/article/details/83795991
https://blog.csdn.net/qq_30051139/article/details/54019515?utm_source=blogxgwz3
https://blog.csdn.net/izard999/article/details/6708738
https://www.cnblogs.com/dolphin0520/p/3933551.html
【1】ConcurrentModificationException 异常解析和快速失败,安全失败的更多相关文章
- java中的ConcurrentModificationException异常
先看这样一段代码: List<String> list = new ArrayList<String>(); list.add("1"); list.add ...
- for、foreach和Iterator区别及ConcurrentModificationException异常
(问:1.for.foreach和Iterator遍历有什么区别 2.遍历删除ConcurrentModificationException异常.) 1.在形式上 for的形式是 for(int ...
- 【原创】快速失败机制&失败安全机制
这是why技术的第29篇原创文章 之前在写<这道Java基础题真的有坑!我求求你,认真思考后再回答.>这篇文章时,我在8.1小节提到了快速失败和失败安全机制. 但是我发现当我搜索" ...
- 线上Kafka突发rebalance异常,如何快速解决?
文章首发于[陈树义的博客],点击跳转到原文<线上Kafka突发rebalance异常,如何快速解决?> Kafka 是我们最常用的消息队列,它那几万.甚至几十万的处理速度让我们为之欣喜若狂 ...
- 一种隐蔽性较高的Java ConcurrentModificationException异常场景
前言 在使用Iterator遍历容器类的过程中,如果对容器的内容进行增加和删除,就会出现ConcurrentModificationException异常.该异常的分析和解决方案详见博文<Jav ...
- java.util.ConcurrentModificationException异常;java.util.ConcurrentModificationException实战
写代码遇到这个问题,很多博客文章都是在反复的强调理论,而没有对应的实例,所以这里从实例出发,后研究理论: 一.错误产生情况 1 .字符型 (1)添加 public static void main(S ...
- Java ConcurrentModificationException异常原因和解决方法
Java ConcurrentModificationException异常原因和解决方法 在前面一篇文章中提到,对Vector.ArrayList在迭代的时候如果同时对其进行修改就会抛出java.u ...
- Java并发编程:Java ConcurrentModificationException异常原因和解决方法
Java ConcurrentModificationException异常原因和解决方法 在前面一篇文章中提到,对Vector.ArrayList在迭代的时候如果同时对其进行修改就会抛出java.u ...
- java集合--java.util.ConcurrentModificationException异常
ConcurrentModificationException 异常:并发修改异常,当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常.一个线程对collection集合迭代,另一个线程对Co ...
随机推荐
- 软件工程第三次作业(One who wants to wear the crown, Bears the crown.)
最大连续子数组和 题目 给定n个整数(可能为负数)组成的序列a[1],a[2],a[3],-,a[n],求该序列如a[i]+a[i+1]+-+a[j]的子段和的最大值.当所给的整数均为负数时定义子段和 ...
- SpringBoot日记——信息修改PUT篇
我们常用的功能,除了post和get,还有put和delete,这篇文章就介绍一下这个put的基本用法. 页面跳转和回显 1. 首先,我们之前的页面已经将添加和修改的按钮都做好了,那么如何实现这些按钮 ...
- 前端项目模块化的实践2:使用 Webpack 打包基础设施代码
以下是关于前端项目模块化的实践,包含以下内容: 搭建 NPM 私有仓库管理源码及依赖: 使用 Webpack 打包基础设施代码: 使用 TypeScript 编写可靠类库 使用 TypeScript ...
- Python提示信息表示内容
=此页面列出了PyLint 1.1.0支持的所有消息,按消息文本排序.还有一个按消息代码排序的所有代码列表. E0001,F0001,W0511(消息不同) E0103:循环中%r不正确W1501 ...
- JSP JSTL知识结构图
自行绘制,欢迎指正.
- SDN交换机迁移2
关于迁移过程中迁移目标(被迁移的交换机和目标控制器)的选择 SDN中基于过程优化的交换机竞争迁移算法 通信学报 交换机:请求速率大于域内平均请求速率的交换机集合: 控制器:综合网络中时延.流量和控制器 ...
- Python之路3【知识点】白话Python编码和文件操作(截载)
无意发现这篇文章讲的比较好,存下来供参考: http://www.cnblogs.com/luotianshuai/p/5735051.html
- 用node编写cli工具
cli是command-line interface的缩写,即命令行工具,常用的vue-cli, create-react-app, express-generator 等都是cli工具. 本文以自己 ...
- python 十进制整数转换为任意进制(36以内)
def baseN(num, b): return ((num == 0) and "0") or (baseN(num // b, b).lstrip("0" ...
- python自动化之PDF
###################################处理PDF和Word文档################################### ''' PDF和Word文档是二进 ...