Exception in thread "main" java.util.ConcurrentModificationException

在Java中,但凡你尝试在Collection下面的集合循环语句中新增/删除元素的时候,都会报错:java.util.ConcurrentModificationException

Map

public static void main(String[] args) {
    class User{
        private int id;
        private String name;
        private User(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }
    
    Map<Integer, User> map = new HashMap<>();
    map.put(1, new User(1, "A"));
    map.put(2, new User(2, "B"));
    map.put(3, new User(3, "C"));
    // 从Map中删除用户名为A的元素
    for (Map.Entry<Integer, User> e : map.entrySet()){
        if (e.getValue().name.equals("A")){
            map.remove(e.getKey());
        }
    }
    map.entrySet().forEach(e -> System.out.println(e.getKey()));
}

报错

Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
  at java.util.HashMap$EntryIterator.next(HashMap.java:1479)
  at java.util.HashMap$EntryIterator.next(HashMap.java:1477)

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

fail-fast:快速失败机制,即当某(多)个线程在循环遍历集合的时候,有其它线程对这个集合进行了修改,就会触发快速失败机制。出现在uitl包下的集合。

fail-safe:安全失败机制,不会出现快速失败的问题。出现在JUC下的集合。比如:执行相同的代码,使用ConcurrentHashMap就不会出错。

回到问题上,我们首先要知道foreach循环的时候,干了什么事情。

foreach循环,是JDK1.5开始提出的新特性,但是隐藏了其内部实现,这就是为什么Debug半天,只路过那几行Iterator的代码。虽然这样,我们只需要知道,编译器在编译的时候将for关键字转化成对目标迭代器的使用

另外,有没有发现,只有Collection接口下的集合类才能使用foreach循环,因为他们都实现了Iterator接口。那么一句话:foreach循环的时候,内部间接调用的是集合类实现的Iterator逻辑

当调用map.entrySet(),进入  java.util.HashMap#entrySet方法

EntrySet类,重写了iterator方法,为foreach循环提供了可能

iterator方法返回的是一个EntryIterator对象,其中实现了next方法;其父类实现了hasNext方法。由此可以想到,foreach循环指向下一个对象就是利用这其中的hasNaet和next方法

hasNext为true,才会执行next方法

而EntryIterator对象中next方法调用的是其父类HashIterator的nextNode方法

到这里,找到了java.util.ConcurrentModificationException的出处

modCount:记录集合被修改的次数

expectedModCount:初始化EntryIterator的时候,等于modCount的值

HashIterator() {
    // 初始化:期望修改次数,等于当前修改次数
    expectedModCount = modCount;
    Node<K,V>[] t = table;
    current = next = null;
    index = 0;
    if (t != null && size > 0) { // advance to first entry
        do {} while (index < t.length && (next = t[index++]) == null);
    }
}

每当循环指向下一个元素的时候

final Node<K,V> nextNode() {
    Node<K,V>[] t;
    Node<K,V> e = next;
    // 如果修改次数不等于期望次数,证明此集合在遍历的过程中被修改了
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
    if ((next = (current = e).next) == null && (t = table) != null) {
        do {} while (index < t.length && (next = t[index++]) == null);
    }
    return e;
}

彩蛋:如果你【新增/删除】的恰好是最后一个元素,则不会抛异常

public static void main(String[] args) {
    class User{
        private int id;
        private String name;
        private User(int id, String name) {
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    Map<Integer, User> map = new HashMap<>();
    map.put(1, new User(1, "A"));
    map.put(2, new User(2, "B"));
    map.put(3, new User(3, "C"));
    // 从Map中删除最后一个元素
    for (Map.Entry<Integer, User> e : map.entrySet()){
        if (e.getValue().name.equals("C")){
            map.remove(e.getKey());
        }
    }
    map.forEach((id, user) -> System.out.println(id + ":" + user));
}

输出:

为什么?如果你看懂了上面的分析,你会知道为什么:

每次指向下一个元素的时候,才会去判断集合是否遭到了破坏,而如果你删除的恰好是最后一个元素,hasNext为false,不会触发相关的判断。

为什么调用集合的remove方法会触发,而调用iterator的remove不会?

public static void main(String[] args) {
    class User{
        private int id;
        private String name;
        private User(int id, String name) {
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    Map<Integer, User> map = new HashMap<>();
    map.put(1, new User(1, "A"));
    map.put(2, new User(2, "B"));
    map.put(3, new User(3, "C"));
    Iterator<Map.Entry<Integer, User>> iterator = map.entrySet().iterator();
    while (iterator.hasNext()){
        Map.Entry<Integer, User> next = iterator.next();
        if (next.getValue().name.equals("A"))
            iterator.remove();
    }
    map.forEach((id, user) -> System.out.println(id + ":" + user));
}

输出:

看一下,iterator的remove方法

public final void remove() {
    Node<K,V> p = current;
    if (p == null)
        throw new IllegalStateException();
    // 如果remove的时候,有其他线程修改了,则同样会抛异常
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    current = null;
    K key = p.key;
    removeNode(hash(key), key, null, false, false);
    // 关键:iterator的最后,重置了expectedModCount
    expectedModCount = modCount;
}

JDK8新特性

public static void main(String[] args) {
    class User{
        private int id;
        private String name;
        private User(int id, String name) {
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    Map<Integer, User> map = new HashMap<>();
    map.put(1, new User(1, "A"));
    map.put(2, new User(2, "B"));
    map.put(3, new User(3, "C"));
    // 针对Collection的集合,提供了removeIf方法
    map.entrySet().removeIf(e -> e.getValue().name.equals("A"));
    map.forEach((id, user) -> System.out.println(id + ":" + user));
}

List

public static void main(String[] args) {
    class User{
        private int id;
        private String name;
        private User(int id, String name) {
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    List<User> list = new ArrayList<>();
    list.add(new User(1, "A"));
    list.add(new User(2, "B"));
    list.add(new User(3, "C"));
    for (User u : list){
        if (u.name.equals("A")){
            list.remove(u);
        }
    }
}

输出:

Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  at java.util.ArrayList$Itr.next(ArrayList.java:859)

---------------------------------------------------------------------------------------------------------------------------

不多BB,上代码

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;
    Itr() {}
    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];
    }
    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();
        }
    }
    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }
    final void checkForComodification() {
        // 修改次数检查
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

和EntryIterator的逻辑是一样的,不一样的是,删除最后一个元素,同样会抛异常

问题出在哪里呢?在于hasNext的判断条件,数组下标是从0开始的,而size是3,终止条件是cursor等于size。也就是说,对于数组,它会多判断一次。

java.util.ArrayList#subList方法

List<E> subList(int fromIndex, int toIndex)

截取从fromIndex开始,toIndex结束的列表,左闭右开 [fromIndex, toIndex)

如果我们截取了子串之后,又对原列表进行增删操作,则会抛出ConcurrentModificationException异常。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    // 截取子列表
    List<Integer> subList = list.subList(1, 2);
    // 修改父列表
    list.add(4);
    // 遍历子列表
    subList.forEach(System.out::println);
}

看一下subList方法

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

这个对象返回的不是一个新的ArrayList对象,而是ArrayList的一个私有内部类,这个内部类里保存了对父列表的引用,以及生成子列表的时候,父列表的modCount

private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;
    private final int parentOffset;
    private final int offset;
    int size;
    SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
        this.parent = parent;
        this.parentOffset = fromIndex;
        this.offset = offset + fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = ArrayList.this.modCount;
    }
......

结构如图:

 

 

而子列表的增删操作是

public void add(int index, E e) {
    rangeCheckForAdd(index);
    // 检查是否发生了修改
    checkForComodification();
    // 对字串的操作,实际上是间接操作了父串
    parent.add(parentOffset + index, e);
    // 更新修改次数
    this.modCount = parent.modCount;
    this.size++;
}
public E remove(int index) {
    rangeCheck(index);
    checkForComodification();
    E result = parent.remove(parentOffset + index);
    this.modCount = parent.modCount;
    this.size--;
    return result;
}

所以直接修改子列表是不会有什么问题的。而修改父列表的话,是不会去同步SubList其中的modCount的,进而当子列表SubList在遍历的时候:

 

获取迭代器的第一步,就是检查修改次数:

 

当前父列表的修改次数是4,因为前面新加了一个元素,而子列表:

 

是3,依然是截取子列表的时候的modCount,不一致,抛异常

 

posted @ 2020-08-06 16:46  露娜妹  阅读(605)  评论(0)    收藏  举报