异常产生

当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。

package reyo.sdk.utils.test.list2;

import java.util.ArrayList;
import java.util.List; public class AddRemoveList { 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);
}
}*/
}
}

出错详情:

异常原因

ArrayList的父类AbstarctList中有一个域modCount,每次对集合进行修改(增添元素,删除元素……)时都会modCount++

而foreach的背后实现原理其实就是Iterator(关于Iterator可以看Java Design Pattern: Iterator),等同于注释部分代码。在这里,迭代ArrayList的Iterator中有一个变量expectedModCount,该变量会初始化和modCount相等,但如果接下来如果集合进行修改modCount改变,就会造成expectedModCount!=modCount,此时就会抛出java.util.ConcurrentModificationException异常

过程如下图:

我们再来根据源码详细的走一遍这个过程

/*
*AbstarctList的内部类,用于迭代
*/
private class Itr implements Iterator<E> {
int cursor = 0; //将要访问的元素的索引
int lastRet = -1; //上一个访问元素的索引
int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量) //判断是否还有下一个元素
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();
}
}
/* *AbstarctList的内部类,用于迭代 */ private class Itr implements Iterator<E> { int cursor = 0; //将要访问的元素的索引 int lastRet = -1; //上一个访问元素的索引 int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量) //判断是否还有下一个元素 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(); } }

作者:MrDTree
链接:http://www.jianshu.com/p/c5b52927a61a
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
根据代码可知,每次迭代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(); }
}

异常的原因很简单,一个线程修改了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等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。

深入理解异常—fail-fast机制

到这里,我们似乎已经理解完这个异常的产生缘由了。
但是,仔细思考,还是会有几点疑惑:

  1. 既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
  2. ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?

我们来看看源码中modCount的注解

/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*
* <p>This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
*/
protected transient int modCount = 0;

我们注意到,注解中频繁的出现了fail-fast
那么fail-fast(快速失败)机制是什么呢?

“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
但是单线程使用不单时也可能抛出这个异常。

java.util.ConcurrentModificationException解决详解的更多相关文章

  1. 国际化,java.util.ResourceBundle使用详解

    java.util.ResourceBundle使用详解   一.认识国际化资源文件   这个类提供软件国际化的捷径.通过此类,可以使您所编写的程序可以:          轻松地本地化或翻译成不同的 ...

  2. java.util.ResourceBundle使用详解

    java.util.ResourceBundle使用详解   一.认识国际化资源文件   这个类提供软件国际化的捷径.通过此类,可以使您所编写的程序可以:          轻松地本地化或翻译成不同的 ...

  3. java.util.ResourceBundle使用详解(转)

    java.util.ResourceBundle使用详解   一.认识国际化资源文件   这个类提供软件国际化的捷径.通过此类,可以使您所编写的程序可以:          轻松地本地化或翻译成不同的 ...

  4. java.util.Scanner应用详解++扫描控制台输入

    java.util.Scanner应用详解   java.util.Scanner是Java5的新特征,主要功能是简化文本扫描.这个类最实用的地方表现在获取控制台输入,其他的功能都很鸡肋,尽管Java ...

  5. (转)java.util.Scanner应用详解

    java.util.Scanner应用详解   java.util.Scanner是Java5的新特征,主要功能是简化文本扫描.这个类最实用的地方表现在获取控制台输入,其他的功能都很鸡肋,尽管Java ...

  6. java.util.ConcurrentModificationException 解决办法

    在使用iterator.hasNext()操作迭代器的时候,如果此时迭代的对象发生改变,比如插入了新数据,或者有数据被删除. 则使用会报以下异常:Java.util.ConcurrentModific ...

  7. java.util.ConcurrentModificationException 解决办法(转载)

    今天在项目的中有一个需求,需要在一个Set类型的集合中删除满足条件的对象,这时想当然地想到直接调用Set的remove(Object o)方法将指定的对象删除即可,测试代码:   public cla ...

  8. java.util.ConcurrentModificationException 解决办法(转)

    今天在项目的中有一个需求,需要在一个Set类型的集合中删除满足条件的对象,这时想当然地想到直接调用Set的remove(Object o)方法将指定的对象删除即可,测试代码:   public cla ...

  9. java.util.concurrent BlockingQueue详解

    什么是阻塞队列? 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列.这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空.当队列满时,存储元素的线程会等待队列可用.阻塞 ...

随机推荐

  1. java 缺憾:异常的丢失

    一.java的异常实现也是又缺陷的,异常作为程序出错的标志决不能被忽略,但它还是可能被轻易地忽略.下了可以看到前一个异常还没处理就抛出下一个异常,没有catch捕获异常,它被finally抛出下一个异 ...

  2. 入门NodeJS

    入门NodeJS https://www.cnblogs.com/dotnetcrazy/p/10118756.html NodeJS 1.环境配置 之前讲ES6的时候有提过一部分Node的知识,简单 ...

  3. error 1044 (42000):access denied for user ''@'localhost' to database 'quickapp' 解决方法

    在虚拟机上重新创建一个数据库时,一直出现这个报错:error 1044 (42000):access denied for user ''@'localhost' to database 'quick ...

  4. 【Ray Tracing The Next Week 超详解】 光线追踪2-2

    Chapter 2:Bounding Volume Hierarchies 今天我们来讲层次包围盒,乍一看比较难,篇幅也多,但是咱们一步一步来,相信大家应该都能听懂 BVH 和 Perlin text ...

  5. ServletContextListener作用

    ServletContext 被 Servlet 程序用来与 Web 容器通信.例如写日志,转发请求.每一个 Web 应用程序含有一个Context,被Web应用内的各个程序共享.因为Context可 ...

  6. 循序渐进学.Net Core Web Api开发系列【2】:利用Swagger调试WebApi

    系列目录 循序渐进学.Net Core Web Api开发系列目录 本系列涉及到的源码下载地址:https://github.com/seabluescn/Blog_WebApi 一.概述 既然前后端 ...

  7. 条件随机场(crf)及tensorflow代码实例

    对于条件随机场的学习,我觉得应该结合HMM模型一起进行对比学习.首先浏览HMM模型:https://www.cnblogs.com/pinking/p/8531405.html 一.定义 条件随机场( ...

  8. BZOJ.2456.mode(绝对众数)

    题目链接 \(Description\) 限制空间(只能保留两个变量),求给定n个数中出现次数超过\(\frac{n}{2}\)的数. \(Solution\) 维护两个变量,\(now\)和\(cn ...

  9. BZOJ.4816.[SDOI2017]数字表格(莫比乌斯反演)

    题目链接 总感觉博客园的\(Markdown\)很..\(gouzhi\),可以看这的. 这个好像简单些啊,只要不犯sb错误 [Update] 真的算反演中比较裸的题了... \(Descriptio ...

  10. lvs+keepalived 02

    LVS keepalived 高可用负载均衡 环境 IP HOSTNAME Describe 192.168.100.30 lvs01 主负载 192.168.100.31 lvs02 备负载 192 ...