其实关于这个问题,在阿里巴巴Java开发手册中有规定:
WHY ?
查看源码之后才知道是因为
那么modCount和expectedModCount是什么呢?
modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。
expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。
expectedModCount表示这个迭代器预期该集合被修改的次数。其值随着Itr被创建而初始化。只有通过迭代器对集合进行操作,该值才会改变。
那么,接着我们看下userNames.remove(userName);方法里面做了什么事情,为什么会导致expectedModCount和modCount的值不一样。
通过翻阅代码,我们也可以发现,remove方法核心逻辑如下:
可以看到,remove方法只修改了modCount,并没有对expectedModCount做任何操作。
之所以会抛出CMException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改!
所以,在使用Java的集合类的时候,如果发生CMException,优先考虑fail-fast有关的情况,实际上这里并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常
使用CopyOnWriteArrayList代替了ArrayList,就不会发生异常。
fail-safe集合的所有对集合的修改都是先拷贝一份副本,然后在副本集合上进行的,并不是直接对原集合进行修改。并且这些修改方法,如add/remove都是通过加锁来控制并发的。
所以,CopyOnWriteArrayList中的迭代器在迭代的过程中不需要做fail-fast的并发检测。(因为fail-fast的主要目的就是识别并发,然后通过异常的方式通知用户)
但是,虽然基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容。如以下代码:
public static void main(String[] args) {
List<String> userNames = new CopyOnWriteArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
Iterator it = userNames.iterator();
for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove(userName);
}
}
System.out.println(userNames);
while(it.hasNext()){
System.out.println(it.next());
}
}
我们得到CopyOnWriteArrayList的Iterator之后,通过for循环直接删除原数组中的值,最后在结尾处输出Iterator,结果发现内容如下:
[hollis, HollisChuang, H]
Hollis
hollis
HollisChuang
H
迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
其他方式
- 使用迭代器(Iterator)+while循环,
- 普通fori循环,
- 使用Java 8中提供的filter过滤(重新生成一个集合)
- 使用增强for循环其实也可以(找到这个元素删除后 break推出循环)
- 使用fail-safe的集合类(ConcurrentLinkedDeque)