单线程并发修改异常
代码如下:
public class CopyOnWriteTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer i = iterator.next();
if(i == 1) {
list.remove(i);
}
}
}
}
有异常抛出:
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 Thread.CopyOnWriteTest.main(CopyOnWriteTest.java:30)
Process finished with exit code 1
此异常出在迭代器上。
大体来说是因为当执行next()
方法的时候会有一个判断,如果modCount != expectedModCount
,就会抛出此异常,初始时这两个是相等的,一旦对集合进行更新操作,modCount
就会加一,所以这两个就不相等了,就会抛出此异常。
涉及到的源码如下:
int expectedModCount = modCount;
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 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++;
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
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
- 我们从
remove()
方法开始,进入到第16
行,我们要移除1
,进入到第24
行,遍历list集合,找到1
之后,通过fastRemove()
方法移除掉1
. - 进入
fastRemove()
方法,第33
行,刚进去他就把modCount
加一,我们看第1
行,这两个值初始是相等的,现在不相等了。删除1
之后,我们继续向下遍历。 - 执行
iterator.next()
方法,进入到第5
行,先运行checkForComodification()
进行检查。 - 检查,进入到第
42
行,判断两个数据是否相等,因为刚才的删除操作使得modCount
加一,现在这两个数不相等了,所以抛出异常。
问:为什么要抛出此异常,或者说,为什么遍历的时候要先对比**modCount
和expectedModCount
答:对于ArrayList
这种线程不安全的集合来说,当多个线程对集合进行修改和读取操作时,往往会造成线程安全问题。**
源码中对参数modCount
是这么解释的:
/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*
* <p>This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
*/
protected transient int modCount = 0;
单线程解决并发修改异常
把list.remove(1)
改成iterator.remove()
即使用Itr
中的remove()
方法,如下:
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
因为第11
行,他把两个参数设置成相等了,这样就能继续遍历了。
多线程并发修改异常
代码如下:
public class CopyOnWriteTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,2));
System.out.println(Thread.currentThread().getName()+":"+list);
},String.valueOf(i)).start();
}
}
}
代码中,有10个线程,每一个线程都对集合进行增加和遍历操作。也会出ConcurrentModificationException
异常,原因和单线程的解释方法一样,当增加操作导致modCount
和expectedModCoun
时,遍历的时候就会抛出异常,至于代码中的迭代器,第7行直接System.out.println(list)
,就可以使用迭代器遍历,因为list后面省略了toString()方法,而ArrayList的toString()方法是在AbstractCollection
里,而ArrayList继承了AbstractList
,AbstractList
继承了AbstractCollection
。
方法如下:
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
可以看到,toString
方法就是使用迭代器对集合进行迭代。
多线程解决并发修改异常
用JUC
下的类来代替,比如用CopyOnWriteArrayList
来代替ArrayList
。