记一次偶然的java.util.ConcurrentModificationException异常
ArrayList集合迭代器删除报错:java.util.ConcurrentModificationException
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>(); arrayList.add("1"); arrayList.add("2"); arrayList.add("3"); arrayList.add("4"); new Thread(()-> { try { Iterator<String> iterator = arrayList.iterator(); while (iterator.hasNext()) { String curString = iterator.next();
if("2".equals(curString)){ iterator.remove(); //java.util.ConcurrentModificationException } } System.out.println(Thread.currentThread() + ":" + arrayList); } catch (Exception e) { e.printStackTrace(); } }).start(); Iterator<String> iterator = arrayList.iterator(); while (iterator.hasNext()) { String curString = iterator.next(); if("2".equals(curString)){ iterator.remove(); } } System.out.println(arrayList);
}
运行结果可能报错:
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.remove(ArrayList.java:865)
查看源码:java.util.ArrayList.Itr,定位报错代码。865行调用checkForComodification()检查并发修改。
再进入checkForComodifacation(),901行 throw new ConcurrentModificationException();
找到原因:当modCount != expectedModCount 时,就会报ConcurrentModificationException异常。
modCount 是抽象类AbstractList的属性,ArrayList继承AbstractList,ArrayList的add和remove方法会使modCount++。
expectedModCount是迭代器Itr类的属性,初始值等于modCount ,用迭代器删除元素后会使expectedModCount=modCount 。
所以,多个线程用迭代器方式遍历同一个集合删除元素时,modCount是共享变量,当一个线程删除元素后modCount++了,导致另一个线程迭代器Itr的expectedModCount比modCount小,调用checkForComodification()时,modCount != expectedModCount ,就会报ConcurrentModificationException异常。
解决方案:
1. 加锁,用反向for循环遍历来删除元素
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
arrayList.add("4");
ReentrantLock reentrantLock = new ReentrantLock();//可重入锁
new Thread(()-> {
try {
reentrantLock.lock();
for (int i = arrayList.size() -1; i >= 0; i--) {
String curString = arrayList.get(i);
if("2".equals(curString)){
arrayList.remove(i);
}
}
System.out.println(Thread.currentThread() + ":" + arrayList);
reentrantLock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
//mian线程
reentrantLock.lock();
for (int i = arrayList.size() -1; i >= 0; i--) {
String curString = arrayList.get(i);
if("2".equals(curString)){
Thread.sleep(1000l);
arrayList.remove(i);
}
}
reentrantLock.unlock();
System.out.println(arrayList);
}
运行结果:由于有锁,所以不会删错元素
2. 使用CopyOnWriteArrayList类代替ArrayList,注意:CopyOnWriteArrayList不支持迭代器删除。
public static void main(String[] args) throws InterruptedException {
List<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
arrayList.add("4");
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList);
new Thread(()-> {
try {
for(String string : copyOnWriteArrayList){
if("2".equals(string)){
copyOnWriteArrayList.remove(string);
}
}
System.out.println(Thread.currentThread() + ":" + copyOnWriteArrayList);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
//mian线程
for(String string : copyOnWriteArrayList){
if("2".equals(string)){
copyOnWriteArrayList.remove(string);
}
}
System.out.println(copyOnWriteArrayList);
}
运行结果:CopyOnWriteArrayList写操作都加了ReentrantLock锁,写的时候会新增一个数组,用复制数组的方式写,读写分离。
CopyOnWriteArrayList不保证数据实时一致性,只保证最终一致性。适合读多写少的场景,因为写的时候内存会占用两个数组。