tips:集合之并发修改异常(ConcurrentModificationException)
1-背景
- 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException。
2-原理
- 迭代器在遍历时会直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
代码: 增强foreach遍历集合
List<Integer> list = new ArrayList<>();
list.add(12);
list.add(13);
list.add(14);
for(Integer value:list){
if(value.intValue()==13){
list.remove(value);
}
}
上述代码: java for增强遍历集合 会重写方法等同于iterator遍历集合
List<Integer> list = new ArrayList<>();
list.add(12);
list.add(13);
list.add(14);
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
// 抛ConcurrentModificationException异常的点: it.next()方法 分析源码
Integer value = it.next();
if(value.intValue()==12){
list.remove(value);
}
}
分析it.next()此处实现源码 【代码位于ArrayList源码的内部类里面】
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();
}
}
// 异常抛出最终位置 【条件: modCount != expectedModCount】
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
追溯变量modCount 与 expectedModCount
- 查看list方法的源码,发现集合list每次操作add,remove,clear都会增加modCount值
public E remove(int index) { ... modCount++; ... }
- 而expectedModCount值在ArrayList的内部类Itr中,在声明初始化时候会将modCount赋值给它
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;
- 所以在上述foreach或者iterator遍历集合时,针对集合list.remove(12)操作后,此时modCount++,expectedModCount值保持不变,所以iterator.next()指向下一个元素时候检查并抛出该异常。
- 核心代码:
Iterator<Integer> it = list.iterator(); while(it.hasNext()){ // 抛ConcurrentModificationException异常的点: it.next()方法 分析源码 Integer value = it.next(); if(value.intValue()==12){ list.remove(value); } }
ArrayList内部类的remove()方法
- 解决方法
- 单线程scenes
Iterator<Integer> iterator2 = list.iterator(); while(iterator2.hasNext()){ Integer value = iterator2.next(); if(value.intValue()==13){ iterator2.remove(); } }
- 解释:上面看到ArrayList的内部类有remove()方法,首先调用ArrayList.remove(index)方法,接着进行赋值操作expectedModCount = modCount 即可保证两者相等
- 核心代码
... ArrayList.this.remove(lastRet); ... expectedModCount = modCount;
- 多线程scenes
- 如果两个线程没有同步锁或者其他措施,也容易产生上述异常,因为一个线程操作时候更改了modCount值,而另外一个迭代时该迭代器的expectedModCount与modCOunt值不一致。解决方法如下:
- 01 迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
new Thread( ()->{ synchronized (list){ Iterator<Integer> it01 = list.iterator(); while(it01.hasNext()){ System.out.println(Thread.currentThread().getName()+" "+it01.next()); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } ).start(); new Thread(()->{ synchronized (list){ Iterator<Integer> it02 = list.iterator(); while(it02.hasNext()){ System.out.println(Thread.currentThread().getName()+" "+it02.next()); if(it02.next().intValue()==14){ it02.remove(); } } } } ).start();
- 02 采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作;原理:CopyOnWriteArrayList也是一个线程安全的ArrayList,其实现在于每次add,remove等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。
// 核心代码 static List<String> list = new CopyOnWriteArrayList<String>(); public static void main(String[] args) { list.add("a"); list.add("b"); list.add("c"); list.add("d"); new Thread() { public void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(Thread.currentThread().getName() + ":" + iterator.next()); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; }.start(); new Thread() { public synchronized void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(Thread.currentThread().getName() + ":" + element); if (element.equals("c")) { list.remove(element); } } }; }.start(); }
3-疑惑
- 1.既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
- 解释:
- fast-fail与fail-safe
- 参考2
- 源码内部有错误检测机制:核心fail-fast即用来检查多线程对线程进行操作造成并发问题。
- ConcurrentModificationException并发修改异常。
- 解释: