您的位置:首页 > 编程语言 > Java开发

Java中出现java.util.ConcurrentModificationException的原理探究和解决办法

2015-02-12 12:02 204 查看
文章开始首先要感谢两位提供思路的伙伴,他们是网易博客ID为“风不可追”的伙伴,博客链接是http://fine36.blog.163.com/blog/static/189251005201258113857343/,第二位是提供了ArrayList注释翻译的伙伴,在知乎上ID为lantaozi,链接为http://www.zhihu.com/question/24086463。非常感谢他们!!!

来说一下应用场景,我拿到一个List< E >类型的供应商列表,循环该列表,将供应商code为某某某的从列表中删除,于是简化一下代码如下:

List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");

for(String s : list){
if(s.equalsIgnoreCase("A")){
list.remove(s);
}
}


这样写就会报错,错误如下:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
at java.util.ArrayList$Itr.next(ArrayList.java:831)
at com.param.ListTest.main(ListTest.java:18)


于是我修改了一下代码,修改后的代码如下:

for(int i=0 ; i<list.size();i++){
if(list.get(i).equalsIgnoreCase("A")){
list.remove(i);
}
}


这样就不报错,我就很诧异,于是开始研究原因,首先开始调试代码:



初始化的list,有5个元素A,B,C,D,E没错,size=5也对,大家注意list中modCount这个变量,执行remove语句之后:



只有B,C,D,E四个元素了,size=4了,说明remove成功,继续进入for循环,此时报错java.util.ConcurrentModificationException,按F5进行step into操作,代码如下(此时进入到ArrayList.class):

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];
}


很明显,hasNext方法返回是true,进入next方法,第一行就是一个方法调用,大家注意,checkForComodification();进入到该方法:

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}


如果modCount不等于expectedModCount的话,就抛出该异常。调试就到这里,抛出了异常,那么很明显是这两个变量不等,不等的原因在哪?继续探究,代码回溯到list.remove(s);F5进入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 remove method that skips bounds checking and does not
* return the value removed.
*/
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; // clear to let GC do its work
}


传递进来的Object对象非空所以进入到else,调用了fastRemove方法删除元素,该方法第一行就对modCount进行了++操作,而初始化数组的时候,或者对数组add的时候:

public void add(E e) {
checkForComodification();

try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}


try里的最后一行,是保证了modCount与expectedModCount两个变量一致的,所以说,remove(s)之后,这两个变量就不等了,抛出异常。这样做的原因是防止遍历列表的时候破坏列表结构,保证线程安全,这种情况应该是在多线程使用的时候居多。

那么问题来了,执行后面一段代码,就是使用index循环的时候怎么不抛出异常呢?我们再来回顾一下代码片段:

for(int i=0 ; i<list.size();i++){
if(list.get(i).equalsIgnoreCase("A")){
list.remove(i);
}
}


remove(i);删除元素之后,一样的对modCount进行了++操作,但是在for循环里边就没有checkForComodification();这个方法了,也就是说不对modCount和expectedModCount进行判断,所以就不抛出异常。这就是这两种for循环的根本区别所在。

还有另外一种循环,迭代

Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
if(iterator.next().equalsIgnoreCase("A")){
iterator.remove();
}
}


这种方式也不会报错,我们看看具体实现方式:

private class Itr implements Iterator<E> {
int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;


初始化迭代器的时候就保证了modCount和expectedModCount的一致性,这招很nice啊!再看remove()的时候:

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}


也保证了这两个变量的一致性,这里更是nice,所以用迭代器的话整个过程都保证了线程同步或者说线程安全,那么就不会报错。

综上所述:

for(String s : list)这种for循环,modCount和expectedModCount不会同步修改,而在循环中会判断两个变量如果不等就抛出异常

for(int i=0;i< list.size();i++)这种for循环,modCount和expectedModCount也不会同步修改,而在循环中不会判断两个变量相等情况,所以不抛出异常

使用迭代器,无论是add操作还是remove操作,都能保证modCount和expectedModCount的一致性,所以不会抛出异常

总结:

在实际开发过程中,一开始我使用了for(int i=0;i< list.size();i++)这种for循环,结果有些元素没法删除,也不报错,实在找不到原因就换了一种for循环,就是for(String s : list)这种for循环,结果报错了,解决思路是,new一个removeList,遍历时候,removeList.add(),等到遍历结束后,在用list.removeAll(removeList)这种方法来删除。代码如下:

//new一个removeList
List<String> removeList = new ArrayList<String>();

for(String s : list){
if(s.equalsIgnoreCase("A")){
removeList.add(s);
}
}
//遍历结束后,删除
list.removeAll(removeList);


最后,希望能帮助大家解决一些困扰,有问题欢迎随时提出~~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: