avatar

谈一谈 ConcurrentModificationException

引言

首先我们先看一下下面的代码:

public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("2".equals(item)) {
list.remove(item);
}
}
}

运行一下发现会抛出ConcurrentModificationException异常,下面我们就来谈一谈ConcurrentModificationException

foreach的底层实现

要了解为啥报ConcurrentModificationException异常,我们先来看看foreach的底层实现。
首先,我们将下面的代码编译生成.class文件

public static void main(String[] args) {
List<String> list = new ArrayList<>();//对集合 , 本质上是iterator迭代器
list.add("1");
list.add("2");
for (String s : list) {
System.out.println(s);
}

int[] intArr = {1, 2};
for (int val : intArr) {//对数组 , 就是用fori的常规方式实现
System.out.println(val);
}
}

然后再用反编译工具打开该·class文件,我们会看到下面的代码

public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("1");
list.add("2");
String s;
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
s = (String) localIterator.next();
System.out.println(s);
}

int[] intArr = {1, 2};
int val;
for (int i = 0; i < intArr.length; i++) {
val = intArr[i];
System.out.println(val);
}

通过上面的代码我们就知道了 foreach在数组上还是使用了原来的fori循环 , 在其他可迭代对象上 , 实则是调用了itearator()方式使用迭代器的方式完成遍历。

源码解析

既然我们知道了foreach的底层实现,那我们就直接来看一下ArrayList.iterator()的源码

public Iterator<E> iterator() {
return new Itr();
}

可以看到返回了一个Itr的类,ItrAbstractList的一个内部类,用于迭代,下面是其代码:

/**
* An optimized version of AbstractList.Itr
*/
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;

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
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();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

根据代码可知,每次迭代list时,会初始化Itr的三个成员变量:

 int cursor = 0;   //将要访问的元素的索引
int lastRet = -1; //上一个访问元素的索引
int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)

接着调用hasNext()循环判断访问元素的下标是否到达末尾。如果没有,调用next()方法,取出元素。
而最上面测试代码出现异常的原因在于,next()方法调用checkForComodification()时,发现了modCount != expectedModCount,抛出异常。
接下来我们看下ArrayList的源码,了解下modCount 是如何与expectedModCount不相等的。

public boolean add(E paramE) {  
ensureCapacityInternal(this.size + 1);
/** 省略此处代码 */
}

private void ensureCapacityInternal(int paramInt) {
if (this.elementData == EMPTY_ELEMENTDATA)
paramInt = Math.max(10, paramInt);
ensureExplicitCapacity(paramInt);
}

private void ensureExplicitCapacity(int paramInt) {
this.modCount += 1; //修改modCount
/** 省略此处代码 */
}

public boolean remove(Object paramObject) {
int i;
if (paramObject == null)
for (i = 0; i < this.size; ++i) {
if (this.elementData[i] != null)
continue;
fastRemove(i);
return true;
}
else
for (i = 0; i < this.size; ++i) {
if (!(paramObject.equals(this.elementData[i])))
continue;
fastRemove(i);
return true;
}
return false;
}

private void fastRemove(int paramInt) {
this.modCount += 1; //修改modCount
/** 省略此处代码 */
}

public void clear() {
this.modCount += 1; //修改modCount
/** 省略此处代码 */
}

上面的代码可以看出,ArrayListaddremoveclear方法都会造成modCount的改变。迭代过程中如何调用这些方法就会造成modCount的增加,使迭代类中expectedModCountmodCount不相等,从而抛出ConcurrentModificationException异常。

解决方案

现在我们已经基本了解了异常的发送原因了。接下来我们来解决它。
假如我就是想在迭代集合时删除集合的元素,怎么办?

单线程环境

细心的朋友会发现Itr中的也有一个remove方法,实质也是调用了ArrayList中的remove,但增加了expectedModCount = modCount;保证了不会抛出java.util.ConcurrentModificationException异常。我们就可以将代码修改为下面这样:

Iterator<String> iter = list.iterator();
while(iter.hasNext()){
String str = iter.next();
if( str.equals("B") ){
iter.remove(); // 关键代码
}
}

但是,这个办法的有两个弊端

  1. 只能进行remove操作,add、clear等Itr中没有。
  2. 而且只适用单线程环境。

多线程环境

在多线程环境下,我们运行下面的代码:

public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

new Thread(() -> {
Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
System.out.println(Thread.currentThread().getName() + ":" + iterator.next());
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
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 ("2".equals(element)) {
iterator.remove();
}
}
}
}.start();

}

结果发现还是会抛异常,异常的原因很简单,一个线程修改了list的modCount导致另外一个线程迭代时modCount与该迭代器的expectedModCount不相等。

此时有两个办法:

  1. 迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
  2. 采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作

CopyOnWriteArrayList也是一个线程安全的ArrayList,其实现原理在于,每次add,remove等操作时,先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。具体参考:聊聊并发-Java中的Copy-On-Write容器

fail-fast机制

到这里,我们似乎已经理解完这个异常的产生缘由和解决方案了。
但是,仔细思考,还是会有几点疑惑:

  1. 既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量?
  2. ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?
    我们来看看源码中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;

我们注意到,注解中频繁的出现了fail-fast
那么fail-fast(快速失败)机制是什么呢?

“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
但是单线程使用不当时也可能抛出这个异常。

文章作者: 毛毛是只猫
文章链接: http://lshaolin.github.io/posts/32b01532/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 毛毛是只猫