一、前言
List集合操作时有时候会出现 java.util.ConcurrentModificationException,翻译过来就是并发修改异常.
先看Java API 中对于ConcurrentModificationException是如何介绍的.
当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
多线程操作时出现 : 某个线程在 Collection 上进行迭代时,通常不允许另一个线性修改该 Collection。
尽管该异常称为并发修改异常,但是当单线程操作不当也是会出现这种异常的!
如果单线程发出违反对象协定的方法调用序列,则该对象可能抛出此异常。
例如,如果线程使用快速失败迭代器在 collection 上迭代时直接修改该 collection,则迭代器将抛出此异常。
二、实例讲解
import java.util.ArrayList;
import java.util.List;
public class RemoveListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String s :
list) {
if (s.equals("1")) {
list.remove(s);
}
}
}
}
首先需要了解的是,增强for循环生成的字节码文件反编译后实际的操作不是简单的for循环,而是应用了Iterator迭代器.
反编译的结果
public class RemoveListTest {
public RemoveListTest() {
}
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("1");
list.add("2");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if (s.equals("1")) {
list.remove(s);
}
}
}
}
下面为了好理解,直接对反编译后的代码进行分析
那么这个代码会报错么?
三、源码分析一波
- list.iterator();
返回一个对象,new的对象实现了Iterator接口.
public Iterator<E> iterator() {
return new Itr();
}
- Itr类的分析
private class Itr implements Iterator<E> {
// 迭代时的光标初始为0
int cursor; // index of next element to return
// 返回的最后一个元素的索引;-1(如果没有返回)
int lastRet = -1; // index of last element returned; -1 if no such
// 预期修改的总数
int expectedModCount = modCount;
Itr() {}
// 光标的大小是否与集合的实际大小相等,相等说明光标的下一个没有值了,返回false,否则说明还有值,返回true
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];
}
// 检查集合中的值是否修改过
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
while循环开始判断光标所在处是否等于集合大小,光标初始为0一定不等与数组的大小2.往下执行,
String s = (String)var2.next();
其中var2.next()
执行会先检查该集合是否被修改过,并没有操作修改过modCount,故继续执行,光标向后移动1位,并将返回String类型的1赋值给s.
- 执行
list.remove(s);
// ArrayList中的remove(Object o)方法
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循环来查看集合中是否有与o相等的值
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
// 找到后,删除该元素
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
// 修改次数+1
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
// 将删去之后的所有元素向前移1位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 集合大小减1,并对最后一个元素赋值null
elementData[--size] = null; // clear to let GC do its work
}
对于if判断"1".equals(“1”)为true,执行remove操作,其中找到集合中与o相等的值后,将会执行
fastRemove(int index)
该方法将modCount的值+1,size的值-1.
删除完成后返回true.
- 程序最后
最后,继续执行while循环,var2.hasNext()方法
将返回true结束循环.
在之前执行next()方法时,cursion + 1 = 1
在上面执行的fastRemove(int index)方法时size - 1 = 1
因此在执行hasNext方法时,cursion!=size
返回false.
结束循环,不会报错
四、扩展分析
1. 实例中添加这个语句呢?list.add("3");
这样系统将会报错Exception in thread "main" java.util.ConcurrentModificationException
因为size为2与cursion不等,继续执行循环.
但是检测到expectedModCount与modCount不相等,则直接报错!
2. 若将if判断语句修改为if (s.equals("2")) { list.remove(s); }
,结果将会怎样呢?
结果是同样会报错 : CME!
当执行到if判断为true时(也就是光标指到第2个元素后,
cursion+1 = 2
).开始执行list.remove(s);
modCount+1 ,size-1 = 1
继续执行var2.hasNext()方法. cursion与size现在的值不相等!那么继续向下执行!
执行next()方法时,会执行checkForComodification()方法.由于modCount已经+1与expectedModCount不相等了,因此将会报错CME!
五、解决方法
对于这种错误如何避免呢?
- 不使用增强for循环
对于这个例子,很明显我们知道异常的产生原因是由于ArrayList中的属性和内部类Itr中的 属性不一致导致的,那么可以假设在for循环和remove操作的时候不设计到Itr类不就得了。 是的,思路很清晰,就这么简单。啥都不说先上代码。
ArrayList<String> strings = new ArrayList<String>();
strings.add("a");
strings.add("b");
strings.add("c");
strings.add("d");
strings.add("e");
for (int i = 0; i < strings.size(); i++) {
String element = strings.get(i);
if("e".equals(element)){
strings.remove(element);
i --;//需要自己手动维护索引
}
}
- 使用Iterator中的remove方法,不要和ArrayList中的remove方法混着搞
基于上面的思路,既然不想和Itr有来望,好吧,看来直接使用Itr类中的remove方法, 使用Itr遍历对象不也是一个好的想法么。上代码。
ArrayList<String> strings = new ArrayList<String>();
strings.add("a");
strings.add("b");
strings.add("c");
strings.add("d");
strings.add("e");
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
String element = iterator.next();
if("e".equals(element)){
iterator.remove();
}
}
若连续使用两次
iterator.remove()
方法, 将会报错java.lang.IllegalStateException
源码分析
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
// 返回的最后一个元素的索引, 没有的话则返回-1
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public void remove() {
// 检查lastRet
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
@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;
// 调用next方法后, 最后会让lastRet设置为游标的位置的值
return (E) elementData[lastRet = i];
}
try {
// 查看ArrayList类中的remove方法源码(后面)
ArrayList.this.remove(lastRet);
cursor = lastRet;
// 调用一次remove方法后, lastRet更改为-1, 若是再次调用remove方法, 则会remove(-1),此时将会报错非法参数异常!
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// ArrayList中的remove方法
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
- 删除元素的时候不再遍历了,直接removeAll 既然异常是对list做遍历和remove操作的时候出现的,好吧,暴力点,我能不遍历的时候做remove操作吗? 好吧,思路正确,满足你
ArrayList<String> strings = new ArrayList<String>();
strings.add("a");
strings.add("b");
strings.add("c");
strings.add("d");
strings.add("e");
ArrayList<String> tempStrings = new ArrayList<String>();
for (String string : strings) {
if("e".equals(string)){
tempStrings.add(string);
}
}
strings.removeAll(tempStrings);
- 其它方法
思路总是多的,比如说加个锁保证数据正确,什么去掉这么到校验自己实现个ArrayList,
怎么地都行,你想怎么玩就怎么玩,方便点的话直接使用java.util.concurrent包下面的CopyOnWriteArrayList。
方法很多,怎么开心就好。
六、总结
《阿里巴巴Java开发手册》
第一章 11.【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
正例 :
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
集合遍历操作时进行删除时,不要使用foreach循环,且删除元素时使用Iterator中的remove方法,而不要使用ArrayList中的remove方法!
下面摘自 : Java集合框架学习(4)——ArrayList中的modCount变量
在单线程环境下确实如此,可当有多个线程对ArrayList进行操作时,线程1进行remove操作后改变了modCount值,但由于modCount不是volatile变量,线程2可能会看到原来的modCount,也可能看到新的modCount,当发现与本线程的expectModCount不同时,仍然会抛出异常。
即使换成Vector容器,可Vector也是继承自AbstractList,仍然会有问题。因此一般有2种解决办法:
在使用iterator迭代的时候使用synchronized或者Lock进行同步;
使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。