美文网首页技术分享collection
ArrayList的remove(Object o)方法 与It

ArrayList的remove(Object o)方法 与It

作者: 闲庭 | 来源:发表于2019-12-30 23:10 被阅读0次

    其实我们日常学习的知识点和问题都是一环扣一环的,很多问题都是相关联的,本次主要围绕着以下问题展开 :

    1. ArrayList的remove(Object o)方法 与Iterator 的remove()方法有什么区别?(会引出Fail-Fast机制)
    2. 什么是Fail-Fast机制与Fail-Safe机制机制?(会引出CopyOnWrite思想)
    3. 什么是CopyOnWrite思想?
    4. CopyOnWrite的使用场景与优缺点?

    其实我们平时也会遇到这样的需求:将列表中满足某种条件的数据删除;稍后让我们借助下面这个逻辑来简单描述下,就是将列表中将等于"1002"的元素删除,此处就拿遍历ArrayList集合当作事例,代码如下:

    private static void testUnsafeArrayList() {
            List<String> arr = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                arr.add("100"+i);
            }
            Iterator it = arr.iterator();
            while (it.hasNext()) {
                String value = String.valueOf(it.next());
                if ("1002".equals(value)){
                    arr.remove(value); //使用这个会抛异常ConcurrentModificationException
    //                it.remove();  //arr.remove(value)替换成 it.remove()没异常
                }
            }
            System.out.println("删除后的数据:");
            Iterator tmp = arr.iterator();
            while (tmp.hasNext()) {
                String value = String.valueOf(tmp.next());
                System.out.println(value);
            }
        }
    

    运行结果:
    执行代码使用 arr.remove(value)你会发现报错如下:

    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 com.liujc.demo.TestUtil.testUnsafeArrayList(TestUtil.java:69)
        at com.liujc.demo.TestUtil.main(TestUtil.java:38)
    
    Process finished with exit code 1
    

    替换方案:
    然后使用it.remove()替换arr.remove(value)即可正常运行:

    这是什么原因呢?
    原因是迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。
    每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,相等的话就返回遍历;否则抛出异常,终止遍历。

    这个理由说的似乎有点道理,我要眼见为实。
    接下来让我们根据源码来分析下:
    首先我们上面代码中利用了ArrayList的remove方法会抛出异常,其实在remove方法中调用了fastRemove, 在fastRemove中执行modCount++;更新了更改次数,查看源码:

    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
        }
    

    在依次遍历迭代器Iterator 时调用其next()方法,通过下面next()源码中可以看到首先调用checkForComodification()方法,该方法主要判断 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];
            }
    final void checkForComodification() {
                 //此处的modCount已经在remove操作中变更
                if (modCount != expectedModCount) 
                    throw new ConcurrentModificationException();
            }
    

    替换方案中使用迭代器的remove方法为什么没有问题?
    查看ArrayList的源码中ListItr继承了Itr,对应的remove()中执行expectedModCount = modCountexpectedModCount重新赋值绕过后续检查,源码如下:

    public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
    
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                //此处重新赋值,会绕过后续next()方法中的checkForComodification检查条件
                expectedModCount = modCount;  
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
          }
    

    其实我们在遇到问题时尽量做到不但要知其然还要知其所以然,要有这种探索精神,但是看源码有时候也要点到为止。好了,回归正题,上面这个案例分析其实已经描述了Fail-Fast 机制的实现原理,那么现在对Fail-Fast 机制是否了解了呢?

    什么是快速失败机制(Fail-Fast)安全失败机制(Fail-Safe)
    也许日常没太注意过这两种机制,但是我们肯定都碰到过,其实在我们平时接触的HashMap、ArrayList 这些集合类,这些在 java.util 包的集合类就都是快速失败的(Fail-Fast);而 java.util.concurrent 包下的类都是安全失败(Fail-Safe),比如:CopyOnWriteArrayList等。

    Fail-Fast机制

    • 什么是Fail-Fast机制?
      Fail-fast 机制是 Java 集合中的一种错误机制。
      在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常。
    • Fail-fast 机制实现原理
      对于实现原理其实根据上面的案例分析更清楚些,这里简单描述下:
      迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。
      每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否与 expectedModCount 值相等,相等的话就返回遍历;否则抛出异常,终止遍历。其实在构造迭代器 Iterator 时,通过 expectedModCount 记录了集合当前(开始遍历元素之前)的修改次数:
      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; //首次初始化迭代器时两者赋值相等,两者不等时说明遍历过程集合数据被修改
      
            Itr() {}
      
            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();
            }
        }
      

    Fail-Safe机制

    • 什么是Fail-Safe机制?
      采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
      由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常。
    • Fail-Safe机制原理:
      在原集合的 copy 上遍历,也就是运用CopyOnWrite思想。
    • 缺点:
      • 创建原集合的 copy 需要额外的空间和时间上的开销;
      • 可能存在脏读的数据,不能保证遍历的是最新的内容。

    CopyOnWrite思想(写入时复制)

    • CopyOnWrite思想概念
      是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明无感知的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源。
    • CopyOnWrite容器
      CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
    • CopyOnWriteArrayList分析
      • 概念:
        并发版ArrayList,底层结构也是数组,和ArrayList不同之处在于:当新增和删除元素时会创建一个新的数组,在新的数组中增加或者排除指定对象,最后用新增数组替换原来的数组。
      • 适用场景:
        由于读操作不加锁,写(增、删、改)操作加锁,因此适用于读多写少的场景。
      • 局限:
        由于读的时候不会加锁(读的效率高,就和普通ArrayList一样),读取的当前副本,因此可能读取到脏数据。

    CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。
    这样做是为了避免在多线程并发add的时候,复制出多个副本出来,可能会出现脏读的情况,导致最终的数组数据不是我们期望的。

        public boolean add(E e) {
         //1、先加锁
         final ReentrantLock lock = this.lock;
         lock.lock();
         try {
             Object[] elements = getArray();
             int len = elements.length;
             //2、拷贝数组
             Object[] newElements = Arrays.copyOf(elements, len + 1);
             //3、将元素加入到新数组中
             newElements[len] = e;
             //4、将array引用指向到新数组
             setArray(newElements);
             return true;
         } finally {
            //5、解锁
             lock.unlock();
         }
        }
    

    线程并发的写,则通过锁来控制,如果有线程并发的读,则分以下几种情况:
    1、如果写操作未完成,那么直接读取原数组的数据;
    2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
    3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

    相关文章

      网友评论

        本文标题:ArrayList的remove(Object o)方法 与It

        本文链接:https://www.haomeiwen.com/subject/dykhoctx.html