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,不一致,抛异常