`
wen303614
  • 浏览: 8087 次
社区版块
存档分类
最新评论

Java ConcurrentModificationException异常原因和解决方法

 
阅读更多

Java ConcurrentModificationException异常原因和解决方法

  在前面一篇文章中提到,对Vector、ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常。下面我们就来讨论以下这个异常出现的原因以及解决办法。

  以下是本文目录大纲:

  一.ConcurrentModificationException异常出现的原因

  二.在单线程环境下的解决办法

  三.在多线程环境下的解决方法

  若有不正之处请多多谅解,并欢迎批评指正

  请尊重作者劳动成果,转载请标明原文链接:

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

一.ConcurrentModificationException异常出现的原因

  先看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
    public static void main(String[] args)  {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==2)
                list.remove(integer);
        }
    }
}

   运行结果:

  

  从异常信息可以发现,异常出现在checkForComodification()方法中。

  我们不忙看checkForComodification()方法的具体实现,我们先根据程序的代码一步一步看ArrayList源码的实现:

  首先看ArrayList的iterator()方法的具体实现,查看源码发现在ArrayList的源码中并没有iterator()这个方法,那么很显然这个方法应该是其父类或者实现的接口中的方法,我们在其父类AbstractList中找到了iterator()方法的具体实现,下面是其实现代码:

1
2
3
public Iterator<E> iterator() {
    return new Itr();
}

   从这段代码可以看出返回的是一个指向Itr类型对象的引用,我们接着看Itr的具体实现,在AbstractList类中找到了Itr类的具体实现,它是AbstractList的一个成员内部类,下面这段代码是Itr类的所有实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private class Itr implements Iterator<E> {
    int cursor = 0;
    int lastRet = -1;
    int expectedModCount = modCount;
    public boolean hasNext() {
           return cursor != size();
    }
    public E next() {
           checkForComodification();
        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();
    }
}

   首先我们看一下它的几个成员变量:

  cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出

  lastRet:表示上一个访问的元素的索引

  expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。

  modCount是AbstractList类中的一个成员变量

1
protected transient int modCount = 0;

   该值表示对List的修改次数,查看ArrayList的add()和remove()方法就可以发现,每次调用add()方法或者remove()方法就会对modCount进行加1操作。

  好了,到这里我们再看看上面的程序:

  当调用list.iterator()返回一个Iterator之后,通过Iterator的hashNext()方法判断是否还有元素未被访问,我们看一下hasNext()方法,hashNext()方法的实现很简单:

1
2
3
public boolean hasNext() {
    return cursor != size();
}

   如果下一个访问的元素下标不等于ArrayList的大小,就表示有元素需要访问,这个很容易理解,如果下一个访问元素的下标等于ArrayList的大小,则肯定到达末尾了。

  然后通过Iterator的next()方法获取到下标为0的元素,我们看一下next()方法的具体实现:

1
2
3
4
5
6
7
8
9
10
11
public E next() {
    checkForComodification();
 try {
    E next = get(cursor);
    lastRet = cursor++;
    return next;
 catch (IndexOutOfBoundsException e) {
    checkForComodification();
    throw new NoSuchElementException();
 }
}

   这里是非常关键的地方:首先在next()方法中会调用checkForComodification()方法,然后根据cursor的值获取到元素,接着将cursor的值赋给lastRet,并对cursor的值进行加1操作。初始时,cursor为0,lastRet为-1,那么调用一次之后,cursor的值为1,lastRet的值为0。注意此时,modCount为0,expectedModCount也为0。

  接着往下看,程序中判断当前元素的值是否为2,若为2,则调用list.remove()方法来删除该元素。

  我们看一下在ArrayList中的remove()方法做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
}

   通过remove方法删除元素最终是调用的fastRemove()方法,在fastRemove()方法中,首先对modCount进行加1操作(因为对集合修改了一次),然后接下来就是删除元素的操作,最后将size进行减1操作,并将引用置为null以方便垃圾收集器进行回收工作。

  那么注意此时各个变量的值:对于iterator,其expectedModCount为0,cursor的值为1,lastRet的值为0。

  对于list,其modCount为1,size为0。

  接着看程序代码,执行完删除操作后,继续while循环,调用hasNext方法()判断,由于此时cursor为1,而size为0,那么返回true,所以继续执行while循环,然后继续调用iterator的next()方法:

  注意,此时要注意next()方法中的第一句:checkForComodification()。

  在checkForComodification方法中进行的操作是:

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

   如果modCount不等于expectedModCount,则抛出ConcurrentModificationException异常。

  很显然,此时modCount为1,而expectedModCount为0,因此程序就抛出了ConcurrentModificationException异常。

  到这里,想必大家应该明白为何上述代码会抛出ConcurrentModificationException异常了。

  关键点就在于:调用list.remove()方法导致modCount和expectedModCount的值不一致。

  注意,像使用for-each进行迭代实际上也会出现这种问题。

二.在单线程环境下的解决办法

  既然知道原因了,那么如何解决呢?

  其实很简单,细心的朋友可能发现在Itr类中也给出了一个remove()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
    }
}

   在这个方法中,删除元素实际上调用的就是list.remove()方法,但是它多了一个操作:

1
expectedModCount = modCount;

   因此,在迭代器中如果要删除元素的话,需要调用Itr类的remove方法。

  将上述代码改为下面这样就不会报错了:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
    public static void main(String[] args)  {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==2)
                iterator.remove();   //注意这个地方
        }
    }
}

三.在多线程环境下的解决方法

  上面的解决办法在单线程环境下适用,但是在多线程下适用吗?看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Test {
    static ArrayList<Integer> list = new ArrayList<Integer>();
    public static void main(String[] args)  {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        Thread thread1 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    System.out.println(integer);
                    try {
                        Thread.sleep(100);
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        Thread thread2 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    if(integer==2)
                        iterator.remove(); 
                }
            };
        };
        thread1.start();
        thread2.start();
    }
}

   运行结果:

  

  有可能有朋友说ArrayList是非线程安全的容器,换成Vector就没问题了,实际上换成Vector还是会出现这种错误。

  原因在于,虽然Vector的方法采用了synchronized进行了同步,但是由于Vector是继承的AbstarctList,因此通过Iterator来访问容器的话,事实上是不需要获取锁就可以访问。那么显然,由于使用iterator对容器进行访问不需要获取锁,在多线程中就会造成当一个线程删除了元素,由于modCount是AbstarctList的成员变量,因此可能会导致在其他线程中modCount和expectedModCount值不等。

  就比如上面的代码中,很显然iterator是线程私有的,

  初始时,线程1和线程2中的modCount、expectedModCount都为0,

  当线程2通过iterator.remove()删除元素时,会修改modCount值为1,并且会修改线程2中的expectedModCount的值为1,

  而此时线程1中的expectedModCount值为0,虽然modCount不是volatile变量,不保证线程1一定看得到线程2修改后的modCount的值,但是也有可能看得到线程2对modCount的修改,这样就有可能导致线程1中比较expectedModCount和modCount不等,而抛出异常。

  因此一般有2种解决办法:

  1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;

  2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。

  关于并发容器的内容将在下一篇文章中讲述。

  参考资料:

  http://blog.csdn.net/izard999/article/details/6708738

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

分享到:
评论

相关推荐

    java.util.ConcurrentModificationException 解决方法

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

    出现java.util.ConcurrentModificationException 问题及解决办法

    在Java编程中,`java.util.ConcurrentModificationException` 是一个常见的运行时异常,通常发生在尝试并发修改集合时。这个异常的产生是由于集合类(如HashMap)的非线程安全特性,当你在一个线程中使用迭代器遍历...

    遍历并批量删除容器中元素出现ConcurrentModificationException原因及处置

    下面详细解析一下这个问题的原因和解决策略: 1. **原因分析**: - 当你使用`for-each`循环(增强for循环)遍历集合时,实际上底层是通过迭代器实现的。但是,如果你在循环内部直接修改集合(比如删除元素),而...

    java 集合并发操作出现的异常ConcurrentModificationException

    在Java编程中,`ConcurrentModificationException`是一个常见的运行时异常,主要出现在多线程环境下对集合类(如List、Set、Map等)进行并发修改时。然而,这个异常不仅限于多线程环境,即使在单线程中,如果在遍历...

    总结java的30个异常及方案

    Java异常处理是编程中至关重要的一个环节,它帮助开发者在程序出错时捕获并处理错误,确保程序的健壮性和稳定性。以下是关于Java异常处理的一些关键知识点,以及针对30个常见Java异常的解决方案: 1. **异常体系**...

    Java常见异常集-Java,Hibernate,Tomcat异常

    在Java编程中,异常处理是不可或缺的一部分,尤其是在开发复杂的企业级应用时,如使用Hibernate进行对象关系映射和Tomcat作为应用服务器。本篇将详细解释一些常见的Java、Hibernate以及与Tomcat相关的异常,帮助...

    java常见的异常

    在Java编程语言中,异常是程序运行时...通过良好的异常处理策略,不仅可以使程序更加健壮,还能提供有价值的错误信息,帮助快速定位和解决问题。在实际开发中,结合日志记录和调试工具,可以更有效地追踪和修复异常。

    Java Web应用中常见的错误和异常问题处理.zip

    在Java Web应用开发中,错误和异常的处理是不可或缺的一部分,因为它们直接影响到程序的稳定性和用户体验。本文将深入探讨Java Web应用中遇到的一些常见错误和异常问题,并提供相应的处理策略。 1. **Servlet和JSP...

    java知识图谱.rar

    理解其内部实现原理,如迭代器、并发修改异常(ConcurrentModificationException)以及各种集合之间的转换,对于编写高效代码至关重要。 5. **微服务知识图谱**: 随着大型系统复杂性的增加,微服务架构成为现代...

    java常见错误集合以及描述

    ### Java常见错误集合以及...通过了解这些异常的原因及解决方案,可以帮助开发者更好地处理异常情况,提高程序的健壮性和稳定性。此外,合理的异常处理策略也是提高开发效率的关键。希望本文能对Java开发者有所帮助。

    Java 常见问题解决集合.docx

    **解决:** 这个错误提示通常意味着Java编译器未能找到指定的类、方法或变量。要解决这个问题,可以按照以下步骤进行排查: - **确认类的导入:** 确保所有必要的类都被正确地导入。比如如果你引用了一个不在标准库中...

    java的list取之方法

    根据提供的文件信息,标题与描述均指向了Java中List的操作方法。然而,给出的部分内容却是一段关于导入Excel文件到Web应用中的代码片段,并没有直接涉及List的具体操作方法。因此,我们将结合标题与描述,围绕Java中...

    java-大数据基础面试思考.pdf

    快速失败(Fail-Fast)迭代器机制是指在用迭代器遍历集合的过程中,如果检测到集合在迭代过程中被修改了,就会迅速抛出ConcurrentModificationException异常。快速失败机制的迭代器是为单线程设计的,因为它们不能...

    axis1.4.1.zip

    标题“axis1.4.1.zip”所指的是一份针对Axis1.4版本的修复补丁包,这个补丁主要是为了解决在Java Development Kit (JDK) 1.8环境下,高并发场景下出现的`ConcurrentModificationException`问题。`...

    java环境报错大全下载地址

    "新建 文本文档.txt"可能是文档列表中的一个文件,通常这种文件名表示未命名或初始状态的文本文件,可能包含报错大全的详细内容,如错误信息、错误原因分析和解决方法等。为了充分利用这份资源,建议打开该文件仔细...

    java学习中的错误查找

    解决方法是熟悉Java语法规则,使用IDE的语法检查功能,及时发现并修正错误。 2. **类型转换错误**:Java中不同数据类型的转换需要特别注意,例如将整型转换为浮点型时,可能会丢失精度;在强制类型转换时,可能会...

    java常见错误分析20例电子书

    通过阅读这本电子书,读者不仅可以掌握Java编程中常见的错误类型,还能学习到预防和解决这些错误的方法,从而提升编程技能和代码质量。无论你是Java新手还是有经验的开发者,这本书都将是你的宝贵参考资料。

    Iterator遍历过程中list删除导致异常

    在Java编程中,`Iterator`是用于遍历集合(如`List`, `Set`等)的重要工具。然而,当我们尝试在使用`Iterator`遍历集合的过程中直接...同时,合理利用Java 8的流API可以有效避免这类异常,提高代码的可读性和安全性。

Global site tag (gtag.js) - Google Analytics