ConcurrentModificationException
April 6, 2019 · View on GitHub
ConcurrentModificationException 如果按照名字来翻译,应该是并发修改异常,比如我们常用的ArrayList是非线程安全的,如果存在多线程同时
修改ArrayList中的元素,那么在遍历集合时,会抛出异常,而不是一直的错下去,通常这个异常用来追踪Bug.(文章结尾有oracle的官方文档说明)
设计目的
This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
使用fail-fast机制在并发修改集合元素内容,如add,remove时,使集合可以自己检测到集合中的元素已经被修改了,后续的操作如果不中断,会产生其他问题,因此,抛出ConcurrentModificationException异常,中断流程。
设计原理
AbstractList中维护了 expectedModCount & modCount这个两个值,在新增,删除元素的时候,就会改变这个两个值,如果Iterator检测到了
expectedModCount != modCount,就会出现ConcurrentModificationException异常(也就是fail-fast处理)
什么时候产生
- 多线程并发修改集合元素时
- 单线程,在遍历元素的时候,删除元素,就会触发这个异常
单线程例子
// jdk 1.8
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for(String i:list){
System.out.println(i);
list.remove("3");
}
}
1
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)
at cn.web1992.utils.demo.collections.ArrayListTest.main(ArrayListTest.java:12)
我们知道Java中的foreach遍历,其实是Java的语法糖,其底层实现依然是Iterator(可使用javap -c 来查看)
而Iterator的实现都是对修改会做fail-fast处理的,ArrayList的Iterator实现是在AbstractList中的一个Itr implements Iterator<E>内部类
list.remove("3")这个方法其实是List自己实现的删除元素的方法,如果想在单线程中避免此异常可以使用Iterator接口中提供的remove方法
多线程例子
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("6");
Runnable r1 = () -> {
Iterator<String> iterator = list.iterator();
System.out.println("iterator start...");
while (iterator.hasNext()) {
String i = iterator.next();
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 添加一个元素
Runnable r2 = () -> {
list.add("666");
System.out.println("add end...");
};
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
日志:
iterator start...
1
2
3
add end...
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at cn.web1992.controller.ConcurrentModificationExceptionTest.lambda$main\$0(ConcurrentModificationExceptionTest.java:37)
at java.lang.Thread.run(Thread.java:748)
在多线程的环境下,t2线程如果在t1遍历的时候,向集合中添加了一个元素,那么就会出现ConcurrentModificationException异常
可使用Lock解决,代码如下:
// Lock
ReentrantLock lock = new ReentrantLock();
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("6");
Runnable r1 = () -> {
try {
// 获取锁
lock.lock();
Iterator<String> iterator = list.iterator();
System.out.println("iterator start...");
while (iterator.hasNext()) {
String i = iterator.next();
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
};
// 添加一个元素
Runnable r2 = () -> {
try {
// 获取锁
lock.lock();
list.add("666");
System.out.println("add end...");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
};
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
这里只是举一个例子,在实际应用中,如果同一时刻在只有一个线程可以访问这个List,效率通常十分低下,可以考虑使用java中的并发集合来解决这个问题,如CopyOnWriteArrayList
CopyOnWriteArrayList适合读多写少的场景
思考
从ConcurrentModificationException可以获取到软件设计过程中,一些经典的设计思想(套路)如Iterator,fail-fast,这些思想在解决问题和分析定位问题的时候,了解其中的原理,帮助巨大,效率奇高。
fail-fast
The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Iterator
Iterator由来