为了保证线程安全,在迭代器迭代的过程中,线程是不能对集合本身进行操作(修改,删除,增加)的,否则会抛ConcurrentModificationException的异常。
public class TestConcurrent {
public static void main(String[] args) {
Collection num = new ArrayList<String>();
num.add("One");
num.add("Two");
num.add("Three");
Iterator<String> iterator = num.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if ("One".equals(element)) {
num.remove(element);
} else {
System.out.println(element);
}
}
}
}
运行结果:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
at java.util.ArrayList$Itr.next(ArrayList.java:791)
at dhp.test.TestConcurrent.main(TestConcurrent.java:16)
源码分析 先来分析下整个代码的执行流程:集合操作中有两个比较重要的指标:modCount和exceptedModCount。 modCount值是用来记录当前集合被修改的次数,每修改一次就进行加1(可以认为为集合的版本号),而exceptedModCount是在iterator初始化是就已经指定的值,值为exceptedModCount = modCount,对于上面的代码,开始就对集合进行了三次add操作,所以modCount=3,当代码执行到第10行时就创建了iterator对象执行exceptedModCount = modCount语句对exceptedModCount进行了赋值操作,此时exceptedModCount=3。具体过程如下:
当执行到while语句时,会对iterator.hasnext()进行判断真假,hasnext访方法实现如下:
public boolean hasNext() {
return cursor != size;
}
cursor又是什么呢?cursor是访问集合时指向元素的游标,开始为0,每执行next语句一次就+1,所以当访问到最后一个元素时cursor就等于size,此时就会结束循环。 之后执行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];
}
接着代码进入if语句执行了remove操作,modCount的值+1,这时再一次循环while语句进入next语句并执行next里的checkForComodifcation方法对modCount和exceptedModCount的值进行判断,此时modCount!= exceptedModCount就抛出了开始的那个异常,这就是报错的原因。
当把if语句里的条件改下面代码又会怎样呢?
if("Two".equals(element)){ //条件发生了改变,对第二个元素进行判断
num.remove(element);
}else {
System.out.println(element);
}
运行结果:
One
只输出了一个值就结束程序了,并没有报错! 当执行第一遍while语句时,执行了next方法,cursor值+1,cursor=1,并输出One 当执行第二遍while语句时,执行了next方法,cursor值+1,cursor=2,并删除了Two这个元素,size也为2,还记得是怎样判断while条件真假的吗?
public boolean hasNext() {
return cursor != size;
}
所以当执行第三遍while语句时,cursor = size,hasnext返回false,结束循环,所以程序“来不及”报错就退出循环了,最后只输出一个元素。
从头到尾来走一遍:
java.util.ConcurrentModificationException
工作中碰到个ConcurrentModificationException。代码如下:
List list = ...;
for(Iterator iter = list.iterator(); iter.hasNext();) {
Object obj = iter.next();
...
if(***) {
list.remove(obj);
}
}
在执行了remove方法之后,再去执行循环,iter.next()的时候,报java.util.ConcurrentModificationException(当然,如果remove的是最后一条,就不会再去执行next()操作了)
下面来看一下源码
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
public interface Collection<E> extends Iterable<E> {
...
Iterator<E> iterator();
boolean add(E o);
boolean remove(Object o);
...
}
这里有两个remove方法
接下来来看看AbstractList
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
//AbstractCollection和List都继承了Collection
protected transient int modCount = 0;
private class Itr implements Iterator<E> { //内部类Itr
int cursor = 0;
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification(); //特别注意这个方法
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch(IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet); //执行remove对象的操作
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount; //重新设置了expectedModCount的值,避免了ConcurrentModificationException的产生
} catch(IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount) //当expectedModCount和modCount不相等时,就抛出ConcurrentModificationException
throw new ConcurrentModificationException();
}
}
}
remove(Object o)在ArrayList中实现如下:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++; //只增加了modCount
....
}
所以,产生ConcurrentModificationException的原因就是:
执行remove(Object o)方法之后,modCount和expectedModCount不相等了。然后当代码执行到next()方法时,判断了checkForComodification(),发现两个数值不等,就抛出了该Exception。
要避免这个Exception,就应该使用remove()方法。
这里我们就不看add(Object o)方法了,也是同样的原因,但没有对应的add()方法。一般嘛,就另建一个List了
解决办法 那么又如何在迭代集合时对集合进行操作呢? 方法一: 使用CopyOnWriteArrayList,而不是ArrayList
Collection num = new CopyOnWriteArrayList();
原理: CopyOnWriteArrayList会先复制一个集合副本,当对集合进行修改时普遍的上修改的是副本里的值,修改完后再将原因集合的引用指向这个副本,避免抛出ConcurrentModificationException异常。 底层源码实现如下:
public boolean add(E e) {
final ReentrantLock lock = this.lock; //重入锁
lock.lock(); //加锁,效果与synchronized一样
try {
Object[] elements = getArray();
int len = elements.length; //得到数组长度
Object[] newElements = Arrays.copyOf(elements, len + 1); //复制元素到一个新数组里
newElements[len] = e; //原数组指向新数组
setArray(newElements);
return true;
} finally {
lock.unlock(); //在finally里解锁是为了避免发生死锁
}
}
方法二: 使用iterator对象的remove方法
if("Two".equals(element)){
iterator.remove(); //修改了此行
}else {
System.out.println(element);
}
上面针对ConcurrentModificationException异常在单线程情况下提出了两种解决方案,但是如果涉及到多线程环境可能就不那么乐观了。
下面的例子中开启两个子线程,一个进行遍历,另外一个有条件删除元素:
public class TestConcurrent {
public static void main(String[] args) {
final Collection<String> num = new CopyOnWriteArrayList<String>();
num.add("One");
num.add("Two");
num.add("Three");
/*
* Iterator<String> iterator = num.iterator(); while
* (iterator.hasNext()) { String element = iterator.next(); if
* ("One".equals(element)) { num.remove(element); } else {
* System.out.println(element); } }
*/
new Thread(new Runnable() {
@Override
public void run() {
for (String string : num) {
System.out.println("遍历集合 value = " + string);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (Iterator<String> it = num.iterator(); it.hasNext();) {
String value = it.next();
System.out.println("删除元素 value = " + value);
if (value.equals("Three")) {
it.remove();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
遍历集合 value = One
删除元素 value = One
遍历集合 value = Two
删除元素 value = Two
删除元素 value = Three
遍历集合 value = Three
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
at java.util.ArrayList$Itr.next(ArrayList.java:791)
at dhp.test.TestConcurrent$1.run(TestConcurrent.java:26)
at java.lang.Thread.run(Thread.java:722)
但是方法一是支持多线程的,修改一下两个地方再次测试:
结果如下:
遍历集合 value = One
删除元素 value = One
遍历集合 value = Two
删除元素 value = Two
遍历集合 value = Three
删除元素 value = Three