foreach中不允许对元素进行add和remove底层原理

简介: foreach中不允许对元素进行add和remove底层原理🍅 Java学习路线:搬砖工的Java学习路线🍅 作者微信公众号:程序员小王🍅 程序员小王的博客:https://www.wolai.com/wnaghengjie/ahNwvAUPG2Hb1Sy7Z8waaF🍅 扫描主页左侧二维码,加我微信 一起学习、一起进步🍅 欢迎点赞 👍 收藏 ⭐留言 📝

一、foreach遍历ArrayList过程中使用 add 和 remove

foreach遍历ArrayList代码:

  @Test
    public void ArrayList(){
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i <10; i++) {
            list.add(i);
        }
        for (Integer integer : list) {
           if(integer==3){
               list.remove(integer);
           }
            System.out.println(integer);
        }
    }

结果:

0.png


结果是出现了ConcurrentModificationException 异常,追踪下抛出异常的位置checkForComodification(ArrayList.java:901)

1.png


这个地方告诉我们如果 modCount 不等于 expectedModCount 的时候,就会抛出这个异常信息,那么这两个参数都代表了什么东西呢?为什么不相等的时候,就会出现异常呢?


二、追根溯源

1、modCount是什么?

单词翻译:modCount 修改计数


底层原理:这时候就要让我们去看源码了,在我们点到这个变量的时候,就会有注释告诉我们了 modCount 是 AbstractList 类中的一个成员变量 ,该值表示对List的修改次数。

2.png


2、expectedModCount 是什么?

单词翻译:expectedModCount 期望的最大计数


底层原理:expectedModCount 是 ArrayList 中的一个内部类——Itr中的成员变量,我们来看下怎么又扯出个内部类Itr。通过反编译可以发现foreach编译后内部是使用迭代器实现 的。


3.png


从源码中可以看到Itr实现了Iterator接口,同时声明了expectedModCount这个成员变量, expectedModCount 表示对ArrayList修改次数的期望值,它的初始值为 modCount。


3、熟悉的checkForComodification方法

checkForComodification翻译:所有方法中都进行了并发 修改的检测


从源码可以看到这个类的next和remove方法里面都调用了一个checkForComodification方法,看到checkForComodification是不是很熟悉,这不就是异常的抛出位置吗。


4.png


checkForComodification方法是通过判断modCount和expectedModCount 是否相等来决定是否抛出并发修改异常。


5.png


4、流程回顾

大致流程如下:当integer为3时,调用了remove方法,remove方法中修改了modCount值,然后再输出integer值,再进入下一次循环,此时hasNext为true,进入循环体第一行代码,调用next方法,next方法再调用checkForComodification方法,然后发现expectedModCount和modCount不一致,最终抛出ConcurrentModificationException 异常。


6.png


也就是说,expectedModCount 初始化为 modCount 了,但是后面expectedModCount 没有修改,而在 remove 和 add 的过程中修改了modCount ,这就导致了执行的时候,通过 checkForComodification 方法来判断两个值是否相等,如果相等了,那么没问题,如果不相等,那就给你抛出一个异常来。


而这也就是我们通俗说起来的 fail-fast 机制,也就是快速检测失败机制。


(1)什么是fail-fast机制?

fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException 异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。


2、fail-fast的出现场景

在我们常见的java集合中就可能出现fail-fast机制,比如ArrayList,HashMap。在多线程和单线程环境下都有可能出现快速失败。


三、避免fail-fast 机制

将上面的代码进行修改:


(1)迭代器实现remove/add

@Test
    public void ArrayList(){
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i <10; i++) {
            list.add(i);
        }
        System.out.println("没有删除元素前"+list.toString());
        //使用迭代器的方向进行remove、add
        ListIterator<Integer> listIterator = list.listIterator();
        while(listIterator.hasNext()){
            Integer integer = listIterator.next();
            if(integer==3){
                listIterator.remove();
                listIterator.add(11);
            }
        }
        System.out.println("删除元素后"+list.toString());
    }

运行结果:

7.png


结果也是显而易见的,我们实现了在 foreach 中进行 add 和 remove 的操作.


这里有个注意点,迭代器使用listIterator和iterator均可,看源码可以知道 listIterator其实使用的ListItr内部类,ListItr是继承了Itr类的,同时自己封了一些方法,例如add,hasPrevious,previous等等。所以代码中的remove方法是Itr类的,add方法是ListItr类的


(2)listIterator和iterator区别:

1.使用范围不同,Iterator可以应用于所有的集合,Set、List和Map和这些集合的子类型。而ListIterator只能用于List及其子类型。


2.ListIterator有add方法,可以向List中添加对象,而Iterator不能。


3.ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator不可以。


4.ListIterator可以定位当前索引的位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。


都可实现删除操作,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。


四、使用CopyOnWriteArrayList

1、CopyOnWriteArrayList介绍

在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,
因此如果每次读取都进行加锁操作,其实是一种资源浪费。
我们应该允许多个线程同时访问 List 的内部数据,毕竟读操作是线程安全的。
这和 ReentrantReadWriteLock 读写锁的思想非常类似,
也就是 读读共享、写写互斥、读写互斥、写读互斥。
JDK中提供了 CopyOnWriteArrayList 类,
相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,
CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是:
写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。


CopyOnWriteArrayList 这个类也是能解决 fail-fast 的问题的,我们来试一下:


 

    @Test
    public void copyOnWriteArrayList(){
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i <5; i++) {
            list.add(i);
        }
        System.out.println("没有删除元素前"+list);
        for (Integer integer : list) {
            if(integer==3){
                list.remove(integer);
                list.add(10);
            }
        }
        System.out.println("删除元素后"+list);
    }

8.png


2、CopyOnWriteArrayList实现add/remove原理:

CopyOnWriteArrayList实现了对这个元素中间进行移除添加 的操作,那么他的内部源码是怎么实现的,实际上很简单,复制


也就是他创建一个新的数组,再将旧的数组复制到新的数组上 ,但是为什么很少有人推荐这种做法,根本原因还是 复制


因为你使用了复制,那么就一定会出现有两个存储相同内容的空间,这样消耗了空间,最后进行 GC 的时候,那是不是也需要一些时间去清理他,所以个人不是很推荐,但是写出来的必要还是有的。


(1)CopyOnWriteArrayList实现add底层原理:

public boolean add(E e) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 元素数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 复制数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 存放元素e
        newElements[len] = e;
        // 设置数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

处理流程如下:

1.获取锁(保证多线程的安全访问),获取当前的Object数组,获取Object数组的长度为length,进入步骤②。
2.根据Object数组复制一个长度为length+1的Object数组为newElements
   (此时,newElements[length]为null),进入下一步骤。
3.将下标为length的数组元素newElements[length]设置为元素e,
  再设置当前Object[]为newElements,释放锁,返回。这样就完成了元素的添加。


(2)CopyOnWriteArrayList实现remove原理:

public E remove(int index) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 获取旧值
        E oldValue = get(elements, index);
        // 需要移动的元素个数
        int numMoved = len - index - 1;
        if (numMoved == 0) // 移动个数为0
            // 复制后设置数组
            setArray(Arrays.copyOf(elements, len - 1));
        else { // 移动个数不为0
            // 新生数组
            Object[] newElements = new Object[len - 1];
            // 复制index索引之前的元素
            System.arraycopy(elements, 0, newElements, 0, index);
            // 复制index索引之后的元素
            System.arraycopy(elements, index + 1, newElements, index,
                                numMoved);
            // 设置索引
            setArray(newElements);
        }
        // 返回旧值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

处理流程如下:

1.获取锁,获取数组elements,数组长度为length,获取索引的值elements[index],
  计算需要移动的元素个数(length - index - 1),若个数为0,则表示移除的是数组的最后一个元素,
  复制elements数组,复制长度为length-1,然后设置数组,进入步骤③;否则,进入步骤②
2.先复制index索引前的元素,再复制index索引后的元素,然后设置数组。
3.释放锁,返回旧值。


3、注意

CopyOnWriteArrayList解决 fail-fast 的问题不是通过迭代器来remove或add元素的,而是通过list本身的remove和add方法,所以add的元素位置也不一样,迭代器是当前位置后面一个,CopyOnWriteArrayList是直接放到最后 。

16.png


相关文章
|
1月前
for in ,for of和forEach的区别
for in ,for of和forEach的区别
40 5
|
3月前
|
索引
foreach,for in和for of的区别
foreach,for in和for of的区别
47 1
|
6月前
|
JavaScript 前端开发
foreach、for in和for of的区别?
foreach、for in和for of的区别?
49 0
|
6月前
|
SQL XML Java
<foreach>元素中collection=list改成collection=array
<foreach>元素中collection=list改成collection=array
|
JavaScript 前端开发 索引
foreach、for in 和for of的区别?
foreach、for in 和for of的区别?
List的remove操作一定要小心!
List的remove操作一定要小心!
Zp
|
Java
List集合的clear方法
List集合的clear方法
Zp
308 0
|
Java 容器
使用Iterator遍历map以及list用法
使用Iterator遍历map以及list用法
169 0
使用Iterator遍历map以及list用法
常见遍历方法 for循环、forEach、map、filter、find、findIndex、some、every
常见遍历方法 for循环、forEach、map、filter、find、findIndex、some、every
178 0
|
算法 安全 Java
Array,List,Set及Map遍历内容的方法探究
Array,List,Set及Map遍历内容的方法探究