研究过java集合的人应该经常在集合源码中见到下面这段代码:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
那么为什么会进行这样的处理呢?还有在什么情况下会触发这个异常?
对于ConcurrentModificationException,通过名字应该能猜的出来,这是一个并发修改产生的异常(这里所谓的修改是指新增或删除)。在集合中这是一种fail-fast(快速失败)机制,目的是在产生并发修改异常时主动抛出异常,而不是任其继续运行产生不可测问题。
两个属性介绍
modCount:进行修改操作的次数,初始为0。
expectedModCount:期望修改的次数,初始为0
在集合(比如ArrayList)中每次新增或删除都会执行modCount++操作
比如:
public E remove(int index) {
rangeCheck(index);
//修改计数加1
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
而在进行迭代遍历操作,获取迭代器时会将这个修改计数赋值给期望修改次数:
private class Itr implements Iterator {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
//将修改计数赋值给期望修改次数
int expectedModCount = modCount;
//....
如果在迭代期间进行集合的新增删除操作,将造成modCount != expectedModCount成立。
对于迭代器iterator,我们都知道它有两个方法hasNext()和next()方法,关于next()方法:
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];
}
每次执行都会先执行一次checkForComodification()方法,那么这个checkForComodification方法又是干什么的呢?
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
好吧,这个方法就是本章开头我们提到的那段代码,如果修改计数和期望计数不相等则抛出异常。那么什么时候会产生这种情况呢?上面其实已经提到:
如果在迭代期间进行集合的新增删除操作,将造成modCount != expectedModCount成立。
针对这种情况,其实可以细分为两种情况:单线程和多项程
当进行迭代时集合进行了修改操作。
例:
public class TestArray {
public static void main(String[] args){
final ArrayList list = new ArrayList();
for(int i=0;i<10;i++){
list.add(i);
}
Iterator iterator = list.iterator();
while(iterator.hasNext()){
Integer val = iterator.next();
list.remove(val);
}
}
}
解决方案
1、在迭代时不使用集合的删除方法,改用迭代器的删除方法 iterator.remove();
2、使用for循环遍历集合
3、使用线程安全的集合比如CopyOnWriteArrayList
4、...
对于多线程,如果其中有线程在迭代遍历集合,另外还有线程同时在修改集合,那么就会产生上述异常
例如:
public class TestArray {
public static void main(String[] args){
final ArrayList list = new ArrayList();
for(int i=0;i<10;i++){
list.add(i);
}
new Thread(){
@Override
public void run() {
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println("遍历集合:"+iterator.next());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
Iterator iterator = list.iterator();
while(iterator.hasNext()){
Integer value = iterator.next();
System.out.println("删除集合:"+value);
iterator.remove();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
结果:
上面的例子在多线程情况下,仅使用单线程遍历中进行删除的第1种解决方案使用it.remove(),但是测试得知上面1、2方案依然会产生同样的异常。
接着来再看一下JavaDoc对java.util.ConcurrentModificationException异常的描述:
当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
说明以上办法在同一个线程执行的时候是没问题的,但是在异步情况下依然可能出现异常。
尝试方案
(1) 在所有遍历增删地方都加上synchronized或者使用Collections.synchronizedList,虽然能解决问题但是并不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
(2) 推荐使用ConcurrentHashMap或者CopyOnWriteArrayList。
以上仅个人理解,如有错误望指正。