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

来源:互联网 发布:联想 知乎 编辑:程序博客网 时间:2024/05/29 11:48

文章开始首先要感谢两位提供思路的伙伴,他们是网易博客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);

最后,希望能帮助大家解决一些困扰,有问题欢迎随时提出~~~

0 0