一、异常原因与异常源码分析

  对集合(List、Set、Map)迭代时对其进行修改就会出现java.util.ConcurrentModificationException异常。这里以ArrayList为例,例如下面的代码:

ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
//遍历1
for (String s : list){
if (s.equals( "3")) {
list.remove(s); // error
}
}
//遍历2
Iterator<String> it = list.iterator();
for (; it.hasNext();) {
String value = it.next();
if (value.equals("3")) {
list.remove(value); // error
}
}

  ArrayList类中包含了实现Iterator迭代器的内部类Itr,在Itr类内部维护了一个expectedModCount变量,而在ArrayList类中维护一个modCount变量(modCount是ArrayList实现AbstractList类得到成员变量)。其他集合(List、Set、Map)都与之类似。

  当对集合进行添加或者删除操作时modCount的值都会进行modCount++操作,例如ArrayList中的remove()方法:

public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
}

  当集合添加完值后,对集合进行遍历时才会创建Itr对象,这时候会执行int expectedModCount = modCount;操作,也就是说只要是在增加或删除后对集合进行遍历,那expectedModCount 与modCount永远是相等的。

  但是如果在遍历的过程中进行增加或删除操作那么modCount++,但是expectedModCount保存的还是遍历前的值,也就是expectedModCount和modCount的值是不相等的。

  遍历过程中会调用iterator的next()方法,next()方法方法会首先调用checkForComodification()方法来验证expectedModCount和modCount是否相等,因为之前做了增加或删除操作,modCount的值发生了变化,所以expectedModCount和modCount不相等,抛出ConcurrentModificationException异常。

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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

二、单线程解决方案

1、迭代器删除

  在Itr类中也给出了一个remove()方法,通过调用Itr类的方法就可以实现而且不报错,例如下面代码:

ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.remove("4");
//遍历2
Iterator<String> it = list.iterator();
for (; it.hasNext();) {
String value = it.next();
if (value.equals("3")) {
it.remove();
}
}

  在Itr类中remove()方法中,执行了expectedModCount = modCount操作,那么执行next()方法时expectedModCount和modCount肯定相等,Itr类中remove()方法的源码:

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();
}
}

2、其他的方式

 // 2 建一个集合,记录需要删除的元素,之后统一删除
List<string> templist = new ArrayList<string>();
for (String value : myList) {
if (value.equals( "3")) {
templist.remove(value);
}
}
// 可以查看removeAll源码,其中使用Iterator进行遍历
myList.removeAll(templist);
System. out.println( "List Value:" + myList.toString()); // 3. 使用线程安全CopyOnWriteArrayList进行删除操作
List<string> myList = new CopyOnWriteArrayList<string>();
myList.add( "1");
myList.add( "2");
myList.add( "3");
myList.add( "4");
myList.add( "5"); Iterator<string> it = myList.iterator(); while (it.hasNext()) {
String value = it.next();
if (value.equals( "3")) {
myList.remove( "4");
myList.add( "6");
myList.add( "7");
}
}
System. out.println( "List Value:" + myList.toString()); // 4. 不使用Iterator进行遍历,需要注意的是自己保证索引正常
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的位置
}
}

三、多线程解决方案

1、多线程下异常原因

  多线程下ArrayLis用Itr类中remove()方法也是会报异常的,Vector(线程安全)也会出现这种错误,具体原因如下:

  Itr是在遍历的时候创建的,也就是每个线程如果遍历都会得到一个expectedModCount ,expectedModCount 也就是每个线程私有的,假若此时有2个线程,线程1在进行遍历,线程2在进行修改,那么很有可能导致线程2修改后导致Vector中的modCount自增了,线程2的expectedModCount也自增了,但是线程1的expectedModCount没有自增,此时线程1遍历时就会出现expectedModCount不等于modCount的情况了。

2、尝试方案

(1) 在所有遍历增删地方都加上synchronized或者使用Collections.synchronizedList,虽然能解决问题但是并不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
(2) 推荐使用ConcurrentHashMap或者CopyOnWriteArrayList。

3、CopyOnWriteArrayList使用注意

(1) CopyOnWriteArrayList不能使用Iterator.remove()进行删除。
(2) CopyOnWriteArrayList使用Iterator且使用List.remove(Object);会出现如下异常:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4"); Iterator<String> it = list.iterator();
for (; it.hasNext();) {
String value = it.next();
if (value.equals("4")) {
it.remove(); // error
}
} Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1040)
at TestZzl.main(TestZzl.java:51)

4、最终解决方案

List<string> myList = new CopyOnWriteArrayList<string>();
myList.add( "1");
myList.add( "2");
myList.add( "3");
myList.add( "4");
myList.add( "5"); 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 (int i = 0; i < myList.size(); i++) {
String value = myList.get(i); System.out.println("删除元素 value = " + value); if (value.equals( "3")) {
myList.remove(value);
i--; // 注意
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();

后续会具体分析一下CopyOnWriteArrayList

参考:

https://www.2cto.com/kf/201403/286536.html

https://www.cnblogs.com/dolphin0520/p/3933551.html

Java并发-ConcurrentModificationException原因源码分析与解决办法的更多相关文章

  1. Java并发编程-ReentrantLock源码分析

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  2. Java并发编程 ReentrantLock 源码分析

    ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(Abst ...

  3. Java并发编程-AbstractQueuedSynchronizer源码分析

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

  4. Java并发编程 LockSupport源码分析

    这个类比较简单,是一个静态类,不需要实例化直接使用,底层是通过java未开源的Unsafe直接调用底层操作系统来完成对线程的阻塞. package java.util.concurrent.locks ...

  5. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  6. 【死磕 Java 集合】— ConcurrentSkipListMap源码分析

    转自:http://cmsblogs.com/?p=4773 [隐藏目录] 前情提要 简介 存储结构 源码分析 主要内部类 构造方法 添加元素 添加元素举例 删除元素 删除元素举例 查找元素 查找元素 ...

  7. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  8. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  9. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

随机推荐

  1. [PHP]Nginx与PHP的文件上传大小限制

    ---------------------------------------------------------------------------------------------------- ...

  2. 在Virtualenv中使用Idle

    原创 在Virtualenv中使用Idle 本文适用于Linux系统,在DebianTesting上测试通过. 关于Virtualenv 先看一段Virtualenv这货的官方介绍: virtuale ...

  3. Nginx深度优化

    简介 1.隐藏版本号2.修改Nginx用户与组3.配置Nginx网页缓存时间4.实现Nginx的日志切割5.配置Nginx实现连接超时6.更改进程数7.配置Nginx实现网页压缩功能8.配置Nginx ...

  4. unity3d API知识点随记

    1.transform.translate是增加transform面板相应的数值x,y,z是以本地坐标系为方向:transform.transformdirection是以世界坐标系为方向,可以去测试 ...

  5. python opencv3 给图片加中文

    转自:https://www.cnblogs.com/arkenstone/p/6961453.html opencv3.2将中文输出到图片上 opencv自带的putText函数无法输出utf8类型 ...

  6. UMD模式的js

    (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define( ...

  7. centos7 增加虚拟网卡

    确认内核是否有tun模块 # modinfo tun yum install tunctl -y 如果找不到 vim /etc/yum.repos.d/nux-misc.repo [nux-misc] ...

  8. php页面开启错误提示

    ini_set("display_errors", "On"); error_reporting(E_ALL | E_STRICT);

  9. matlab stereo_gui立体标定

    http://www.vision.caltech.edu/bouguetj/calib_doc/index.html#examples 文档中举了几个例子,有关双目的是第5个, 这个例子展示了如何使 ...

  10. Spring 手动 提交事务

    1.配置文件 applicationContext.xml: <bean id="txManager"class="org.springframework.jdbc ...