初始化
static ArrayList students = new ArrayList();
//初始化
//添加部分学生对象实例
static {
students.add(new Student("张三", "男", 17));
students.add(new Student("李四", "男", 18));
students.add(new Student("王五", "女", 19));
students.add(new Student("云九", "男", 23));
}
查找:
//查找“云九”
public static void find(){
for (Student student : students) {
if(student.getName().equals("云九")){
System.out.println(student.toString());
}
}
}
运行结果
修改:
//找到“云九”,将其年龄修改为18
//修改
public static void modify(){
for (Student student : students) {
if(student.getName().equals("云九")){
student.setAge(18);
System.out.println(student.toString());
}
}
}
运行结果
添加:
//如果有“云九”,则添加一个学生“肖八”
//添加学生
public static void add(){
for (Student student : students) {
if(student.getName().equals("云九")){
students.add(new Student("肖八", "男", 22));
}
}
}
运行结果
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
删除:
//找到“云九”,将其删除
public static void delete(){
for (Student student : students) {
if(student.getName().equals("云九")){
students.remove(student);
}
}
}
运行结果
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
由此,ArrayList的四种基本操作增删改查,其中的增和删都出现了ConcurrentModificationException,也就是并发修改异常,仔细查看抛出的异常,我们可以发现错误都指向了ArrayList内部类Itr的next方法当中的checkForComodification方法
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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
看到next方法,首先联想到迭代器Iterator,但是以上都是通过for-each进行遍历的,所以这里还涉及到for-each的内部实现问题, for-each的底层其实也是通过迭代器实现的(java foreach内部实现_山间明月江上清风_的博客-CSDN博客),也就是说当我们在使用简洁for循环或者通过迭代器遍历时,都会先调用迭代器的next方法,在next方法中又会首先调用checkForComodification()方法进行检查,当这个方法检查到modCount与expectedModCount不相等时,就会抛出并发修改异常。
由此可知,并发修改异常的实质是一个名为modCount的变量值不再等于expectedModCount的变量值
modCount
进一步查看源码可以知道modCount是定义在AbstractList抽象类中的成员变量,而ArrayList是此类的子类,所以ArrayList继承到了modCount这个变量
源码中对其的解释为:
modCount代表该列表在结构上被修改的次数。如果该字段的值发生意外变化,迭代器将抛出并发修改异常,以响应next、remove、set或add等对集合执行的操作。
expectedModCount
expectedModCount是ArrayList内部类Itr中的成员变量,当ArrayList对象调用iteroter()方法时,会自动创建内部类Itr的对象,并给其成员变量expectedModCount赋初始值,初始值为modCount!即当最初调用迭代器方法时,expectedModCount 和 modCount 值的是默认相等的
关系:
当我们创建ArrayList对象的时候,ArrayList对象里包含了此变量modCount并且初始化值为0;
通过查看源码,我们能发现在ArrayList类中有操作modCount的方法都是添加元素的相关功能和删除元素的相关功能。由此可以得出结构修改的解释,是指那些改变列表大小的操作,最开始执行的查看和修改的操作,在实质上并没有改变存储学生的数量
通过以上源码可以发现:每删除一个元素,modCount的值会自增一次;每添加一个元素,modCount的值也会自增一次,我们每次执行对集合中的元素数量产生变化的操作时,modCount的值就会+1,但是这个操作仅限于增删元素,修改元素值并不会影响modCount的值,再结合API中对此变量的解释,我们可以得出结论: modCount变量代表了对集合元素个数的改变次数
异常分析:
上文中已经提到过,当ArrayList对象调用iteroter()方法时,会创建内部类Itr的对象。此时迭代器对象中有两个最关键的成员变量:cursor、expectedModCount
cursor:
迭代器的工作就是将集合中的元素逐个取出,而cursor就是迭代器中用于指向集合中某个元素的指针,在迭代器迭代的过程中,cursor初始值为0,每次取出一个元素,cursor值会+1,以便下一次能指向下一个元素,直到cursor值等于集合的长度为止,从而达到取出所有元素的目的。expectedModCount:
expectedModCount在迭代器对象创建时被赋值为modCount,当创建完迭代器对象后,如果我们没有对集合结构进行修改,expectedModCount的值是一直等于modCount的值的,在迭代集合元素的过程中,迭代器通过检查expectedModCount和modCount的值是否相同,以防止出现并发修改,如果这两个值不相等,就会抛出并发修改异常。
//Iterator中的remove方法
public static void delete01(){
Iterator iterator = students.iterator();
while (iterator.hasNext()) {
Student next = iterator.next();
if(next.getName().equals("云九")){
iterator.remove();
}
}
//遍历查看
Iterator iterator1 = students.iterator();
while (iterator1.hasNext()) {
Student next = iterator1.next();
System.out.println(next.toString());
}
}
运行结果
原因:
不要和List中的remove混用!迭代器封装的一个自己的remove方法,可以看到这是Iterator接口的默认方法
进一步查看Iterator接口的实现类中,ArrayList重写的Iterator接口的remove方法
可以发现Iterator接口的remove方法在最后一步多了一个操作,expectedModCount = modCount,也就是说使用迭代器的remove方法时,当modCount发生改变后,expectedModCount也会跟着改变,下一次检查时必然是相等的!所以不会出现并发修改异常!
//ListIterator
public static void delete02(){
ListIterator listIterator = students.listIterator();
while (listIterator.hasNext()) {
Student next = listIterator.next();
if(next.getName().equals("云九")){
listIterator.remove();
}
}
//查看遍历
Iterator iterator1 = students.iterator();
while (iterator1.hasNext()) {
Student next = iterator1.next();
System.out.println(next.toString());
}
}
运行结果
原因:
从原码中可以发现ListIterator是Iterator的子接口,查看ArrayList重写的ListIterator接口的remove方法
同样可以发现ListIterator接口的remove方法在最后一步多了一个操作,expectedModCount = modCount操作
仔细对比,可以发现Iterator的remove方法和ListIterator的remove方法是完全一致的!然后仔细对比源码可以发现ArrayList类的remove方法(872行~885行)实质上实现了对Iterator和ListIterator两个接口的remove的重写,也就是说Iterator和ListIterator实质上用的是同一个重写后的remove方法!所以这两种方法避免并发修改异常的原理是完全相同的!
//removeIf()
public static void delete03(){
students.removeIf(student -> student.getName().equals("云九"));
//查看遍历
Iterator iterator1 = students.iterator();
while (iterator1.hasNext()) {
Student next = iterator1.next();
System.out.println(next.toString());
}
}
运行结果
原因:
从源码中可以知道,自JDK1.8后,Collection以及其子类新加入了removeIf方法,作用是按照一定规则过滤集合中的元素
在ArrayList类中重写的removeif方法中,我们依旧可以看到expectedModCount = modCount的操作
查看Collection中removeIf的重写方法,可以发现能够使用removeIf方法的类很少,所以removeIf方法的使用具有一定的局限性(1.实现了Collection接口 2.该集合必须重写了Collection接口removeIf方法 3.重写的removeIf方法中必须有expectedModCount = modCount的操作)
普通for循环只有在能通过下标获取元素的集合中可用,因为他不涉及迭代器的next方法,不会检查modCount的值。
当在某种特定条件存在的情况下删除一个元素时,可以通过contains()方法;删除多个元素时可以通过新创建一个容器B存放所有需要删除的元素,通过遍历新容器B来删除旧容器A中的值,此时遍历的是A,遍历过程中检查的也是A的modCount值,所以删除B中的元素并不会报并发修改异常。
//使用Stream流过滤
public static void delete05(){
List del = students.stream().filter(student -> !student.getName().equals("云九")).collect(Collectors.toList());
//查看遍历
Iterator iterator = del.iterator();
while (iterator.hasNext()) {
Student next = iterator.next();
System.out.println(next.toString());
}
}
运行结果:
实质上是将students中名字不等于“云九”的学生复制到新的容器del中,将“云九“在复制的过程中过滤掉了,但是该操作具有极大局限性,Collectors 是Java8加入的操作类,位于 java.util.stream 包下。它会根据不同的规则将元素收集归纳起来,比如最简单常用的是将元素装入Map、Set、List 等可变容器中,也只能装入Map、Set、List 中,所以过滤后得到的新的容器只能是Map、Set或者List,而不是原来的容器类型
将del赋给原容器:
//使用Stream流过滤
public static void delete05(){
List del = students.stream().filter(student -> !student.getName().equals("云九")).collect(Collectors.toList());
//清空原集合
students.clear();
//将del添加到原集合中
students.addAll(del);
//查看遍历
Iterator iterator = students.iterator();
while (iterator.hasNext()) {
Student next = iterator.next();
System.out.println(next.toString());
}
}
在讲解并发修改异常时全程以ArrayList为例,有人可能说是因为ArrayList是非线程安全的,换成线程安全的Vector就没问题了,实际上换成Vector还是会出现这种错误。原因在于,虽然Vector的方法采用了synchronized进行了同步,但是实质上每个线程里面用到的都是不同的iterator,因为遍历的是同一个容器,所以最开始的容器的modCount和不同线程中各自的expectedModCount值都是相等的,但是expectedModCount是每个线程各自私有的(一对多,一个modCount对应多个线程的expectedModCount值)。
假设此时有2个线程,线程1在进行遍历,线程2在进行修改或者删除,那么线程2修改后导致modCount自增了,线程2的expectedModCount没有发生变化,同时线程1的expectedModCount也没有发生变化,此时遍历仍旧会出现并发修改异常的情况。
public static void main(String[] args) {
//遍历线程
Thread thread1 = new Thread(){
public void run() {
Iterator iterator = students.iterator();
while(iterator.hasNext()){
Student next = iterator.next();
System.out.println(next);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//删除线程
Thread thread2 = new Thread(){
public void run() {
Iterator iterator = students.iterator();
while(iterator.hasNext()){
Student next = iterator.next();
if(next.getName().equals("云九")){
students.remove(next);
}
}
}
}
thread1.start();
thread2.start();
}
运行结果
假定线程2使用的是迭代器的remove方法,线程1在进行遍历,线程2在进行修改或者删除,那么线程2修改后导致modCount自增了,线程2的expectedModCount在迭代器的remove方法中的expectedModCount = modCount的操作下也自增了,但是线程1的expectedModCount没有发生变化,此时遍历仍旧会出现并发修改异常的情况。
public static void main(String[] args) {
//遍历线程
Thread thread1 = new Thread(){
public void run() {
Iterator iterator = students.iterator();
while(iterator.hasNext()){
Student next = iterator.next();
System.out.println(next);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
};
//删除线程
Thread thread2 = new Thread(){
public void run() {
Iterator iterator = students.iterator();
while(iterator.hasNext()){
Student next = iterator.next();
if(next.getName().equals("云九")){
//迭代器删除
iterator.remove();
}
}
};
};
thread1.start();
thread2.start();
}
运行结果
对整个迭代器进行线程同步操作,当某个线程中的迭代器执行完全部代码时,其他线程中的迭代器才能执行
以并发容器CopyOnWriteArrayList为例,CopyOnWriteArrayList是ArrayList的线程安全版本,内部也是通过数组实现,相当于一个副本,每次修改都会拷贝一份新的来修改,修改完了再替换掉原来的
使用迭代器遍历时,使用集合对象的remove方法删除倒数第二个元素,不会报并发修改异常
Itertor中维护了cursor, lastRet,expectedModCount三个成员变量,
//迭代器在ArrayList中的内部实现类
private class Itr implements Iterator {
int cursor; // 下一个元素的索引
int lastRet = -1; // 上一次取出的元素的索引
int expectedModCount = modCount; // 记录迭代器被创建后的集合结构改变的次数
// .....
}
// 判断是否还能继续遍历,当cursor=size时,后续再取值或者删除都会越界
public boolean hasNext() {
return cursor != size;
}
public E next() {
//判定expectedModCount与modCount之间是否相等,如果不相等,则抛出异常
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; // 加一是为了使cursor变成下一次遍历要取的值的索引
return (E) elementData[lastRet = i]; // 给lastRet赋值,此时的i已经变成了上一次取出值的索引
}
final void checkForComodification() {
if (modCount != expectedModCount)
// 当不相等时,跑出异常
throw new ConcurrentModificationException();
}
当在删除倒数第二个元素的时候,cursor指向最后一个元素的,而此时删掉了倒数第二个元素后,cursor和size()正好相等了,所以hasNext()返回false,遍历结束,这样就成功的删除了倒数第二个元素了。
辅助理解:假设一个装有8个元素的容器,当我们遍历到倒数第二个元素,cursor的值是7,删除完倒数第二个元素的时候,modCount就自增了一次,而容器存储的元素个数也从8变成了7,接着迭代器调用hasNext()方法判断cursor和size是否相同,结果发现相同都是7,所以返回false,while循环就直接终止了,不会再调用next()方法,所以也就不会再检查modCount值,所以不会再报并发修改异常
ConcurrentModificationException异常是由fail-fast机制产生的,java.util包下面的所有的集合类都是快速失败的,“快速失败” 也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有 可能会产生fail-fast机制。记住是有可能,而不是一定!
ConcurrentModificationException异常出现的原因
遍历ArrayList时如何正确移除一个元素
Iterator和ListIterator的区别是什么?
ListIterator有add()方法,可以向List中添加对象,而Iterator不能
ListIterator和Iterator都有hasNext()和next()方法,可以实现按顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历,Iterator就不可以,即Iterator只可以向前遍历,而LIstIterator可以双向遍历
ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现,Iterator没有此功能
都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改
通过迭代器fail-fast属性,你明白了什么?
每次我们尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail-fast机制来设计的(ConcurrentHashMap和CopyOnWriteArrayList这类并发集合类除外)。
fail-fast与fail-safe有什么区别?
fail-fast:直接在容器上进行遍历,在遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException异常导致遍历失败。java.util包下的集合类都是快速失败机制的, 常见使用fail-fast方式遍历的容器有HashMap和ArrayList等
fail-safe:这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等
在迭代一个集合的时候,如何避免ConcurrentModificationException?
在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList
并发集合类是什么?
Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。常见的有CopyOnWriteArrayList、ConcurrentHashMap、CopyOnWriteArraySet