ArrayList的并发修改异常

        在看这篇文章之前建议看下之前的内部类的源码分析   

ArrayList-1-迭代器内部类

1 ArrayList的并发修改异常

  当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。

2 现象

  现象一

  遍历List集合时删除或者添加元素时会出现并发修改异常。

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
for (String str : list) {
    if("C".equals(str)){
        list.remove(str);
    }
    System.out.println(str);
}

控制台:直接抛出了ConcurrentModificationException。

A
B
C
Exception in thread "main" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
 at java.util.ArrayList$Itr.next(ArrayList.java:851)
 at com.dylan.local.Demo01.main(Demo01.java:47)

  现象二

  一个线程在遍历集合的时候,其他线程修改了集合的结构,也会出现并发修改异常。

ArrayList<Integer> al = new ArrayList<>();
for (int i = 0; i < 10; i++)
    al.add(i);

new Thread(() -> {
    try {
        Iterator<Integer> it = al.iterator();
        while (it.hasNext()) {
            // 每次遍历睡1毫秒
            TimeUnit.MILLISECONDS.sleep(1);
            System.out.print(it.next() + " ");
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

new Thread(() -> {
    try {
        // 睡个3毫秒
        TimeUnit.MILLISECONDS.sleep(3);
        al.remove(6);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

控制台:直接抛出了ConcurrentModificationException。

0 1 Exception in thread "Thread-0" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
 at java.util.ArrayList$Itr.next(ArrayList.java:851)
 at com.dylan.local.Demo01$1.run(Demo01.java:68)
 at java.lang.Thread.run(Thread.java:748)

  增强for循环的内部使用的是iterator迭代器实现的,在遍历的时候,修改了集合的结构,上面的代码在运行的时候会报出java.util.ConcurrentModificationException并发修改异常。

增强for循环底层其实就是迭代器实现的

  直接把现象一的class文件反编译就可以看到还是使用Iterator的:

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

String str;
for(Iterator var2 = list.iterator(); var2.hasNext(); System.out.println(str)) {
    str = (String)var2.next();
    if ("C".equals(str)) {
        list.remove(str);
    }
}

3 异常如何产生的

  我们需要结合ArrayList集合源码分析,详细请参见我前面的文章。

  首先,抛出并发修改异常与modCount变量有关,此变量是继承自AbstractList类,它记录着集合结构被修改的次数。我们观察源码可以发现集合的增加和删除等修改了集合结构的方法都会增加modCount变量的值。

  针对上面出现的异常,我们结合源码分析:

  ArrayList集合有一个内部类Itr,该类实现了Iterator接口,它的成员变量如下:

//cursor指向下一个元素
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类的next()方法,hasNext()方法,remove()方法简单介绍下,详细请见上篇文章。

  在next和remove方法中,都有判断expectedModCountmodCount 的值是否相等的方法checkForComodification()。不相等就会抛出并发修改异常。

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


  • hasNext()判断是否还有下一个元素,原理是判断下一个元素的索引是否等于集合的大小。当cursor指向size位置的时候,说明已经到集合的最后一个元素的后面了,也就是没有元素可以迭代了。

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


  • next()返回当前迭代的元素。首先会判断 expectedModCount 和 modCount 是否相等,不相等就抛出并发修改异常。再判断cursor的值是否小于集合和缓冲数组的长度。然后cursor和lastRet都加1,指向的位置后移一位。

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


  • remove():首先判断lastRet是否小于0,再检查expectedModCount 和 modCount 的值是否相等,不相等抛出并发修改异常。然后调用ArrayList的remove方法移除当前索引的元素,将当前索引值赋值给cursor变量,使lastRet变量重置为-1。
  • 调用ArrayList的remove方法会增加modCount 的值,所以此方法的最后会将新的modCount 的值赋值给expectedModCount 。

lastRet置为-1的原因是删除元素后导致lastRet指针指向的元素不正确了。

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

原因分析

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
Iterator<String> it = list.iterator();
while(it.hasNext()){
    String next = it.next();
    if(Objects.equals(next,"C")){
        list.remove(next);
    }
}

接下来分析错误代码运行过程:

  ①在上面错误实例代码中,初始情况如下图所示,在开始遍历之前,lastRet指向-1的位置,cursor指向第一个元素。

ArrayList的并发修改异常

  ②当next()方法获取到元素C的时候,此时lastRet指向索引为2的元素C,cursor指向索引位3的元素D。

                   ArrayList的并发修改异常

  ③此时调用了ArrayList集合的remove()方法,(不是Itrremove哦),而ArrayListremove方法会使modCount变量自增,此时expectedModCounthmodCount就不相等了。此时的集合内部就如下图所示:

list.remove(next);

       ArrayList的并发修改异常

     ④此时迭代器需要遍历下一个元素,就会调用hasNext()方法判断集合中还有无元素,此时cursor的值为3,而集合大小size为4,所以会执行next()方法,此时expectedModCounthmodCount并不相等了,所以会抛出java.util.ConcurrentModificationException并发修改异常。

4 解决办法

  下面前两种方案只是在单线程的情况下使用的,在多线程的情况下建议使用CopyOnWriteArrayList。

使用Iterator的remove方法

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        list.add("E");

        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String str = it.next();
            if("C".equals(str)){
                it.remove();
            }
        }
        System.out.println(list); // [A, B, D, E]
    }

  使用ArrayList的内部类Itr自带的remove方法可以在遍历集合的时候安全的删除数据。原因是在这个方法内部是在调用ArrayList的remove方法后,使cursor的值变为lastRet的值,相当于-1操作 。使lastRet变量重置为默认值-1最后最重要的是使expectedModCount变量等于更新后的modCount,这样就不会抛出并发修改异常了。

  上面的代码的流程和上面错误代码的区别就只是在第③步:

ArrayList的并发修改异常

  但是Itr类只能进行remove删除操作,无法进行add增加和clear清空操作。

  而且remove方法必须在next方法之后使用,而且在一次next方法后只能使用一次remove方法。这些问题都是因为lastRet变量重置为-1引起的。

使用ListIterator的remove方法

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("A");
    list.add("B");
    list.add("C");
    list.add("D");
    list.add("E");

    ListIterator<String> it = list.listIterator();
    while (it.hasNext()){
        String str = it.next();
        if("C".equals(str)){
            it.remove();
        }
    }
    System.out.println(list); // [A, B, D, E]
}

注意:ListIterator类除了有remove方法,还有set和add等方法。

使用线程安全的集合

  可以使用线程安全的集合,例如Vector和CopyOnWriteArrayList。尽量使用CopyOnWriteArrayList。

5 思考

  下面两段代码会不会报错呢?

代码1:

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");

for (String str : list) {
    if("A".equals(str)){
        list.remove(str);
    }
}

代码2:

ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");

for (String str : list) {
    if("B".equals(str)){
        list.remove(str);
    }
}

  结果是第一段代码不会抛异常,第二段代码抛出并发修改异常。

  第一段代码不抛异常的原因是:当遍历到元素A时,调用集合的remove方法吧元素A移除了,此时集合的size减1变为1,cursor加1变为1。此时在hasNext()方法中cursorsize相等,则退出遍历了。所以不报错。

  第二段代码:遍历到元素A,调用集合的remove方法吧元素A移除了,此时集合的size减1变为1,cursor加1变为2。此时hasNext()方法中cursorsize不相等,则会进入next()方法,此时expectedModCounthmodCount并不相等了,所以会抛出java.util.ConcurrentModificationException并发修改异常。

6 小结

  发生并发修改异常可能有两种情况。

  一种是单线程下,在使用迭代器遍历ArrayList集合的时候,使用了ArrayList的方法修改了集合的结构导致的。

  另外一种是,在多线程下,一个线程在遍历集合的时候,另外一个线程修改了集合的结构

  在单线程的情况下可以使用Iterator和ListIterator的方法来解决,多线程下尽量使用线程安全的集合。


原文始发于微信公众号(肝帝笔记):ArrayList的并发修改异常

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/31525.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!