zoukankan      html  css  js  c++  java
  • 【1】ConcurrentModificationException 异常解析和快速失败,安全失败

    目录

    一、引起异常的代码

    二、foreach原理

    三、从ArrayList源码找原因

    四、单线程解决方案

    五、在多线程环境下的解决方法

    一、引起异常的代码

    以下三种的遍历集合对象时候,执行集合的remove和add的操作时候都会引起java.util.ConcurrentModificationException异常。

    注:set方法不会导致该异常,看了源码set没有改变modcount。快速失败迭代器在遍历时不允许结构性修改,javadoc中对此的解释是“结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。”

    public class Test {
    
        public static void main(String[] args) {
    
            List<String> list = new ArrayList<String>();
            list.add("a");
            list.add("b");
            list.add("b");
            list.add("b");
    
            // foreach循环
            for (String str : list) {
                if (str.equals("b")) {
                    list.remove(str);
                }
            }
    
            // for循环借助迭代器遍历Collection对象
            for (Iterator<String> it = list.iterator(); it.hasNext();) {
                String value = it.next();
                if (value.equals("b")) {
                    list.remove(value);
                }
            }
    
            // 迭代器遍历
            Iterator<String> it = list.iterator();
            while (it.hasNext()) {
                String value = it.next();
                if (value.equals("3")) {
                    list.remove(value);
                }
            }
            System.out.println(list);
        }
    }

    抛出的异常:

    从异常信息可以发现,异常出现在checkForComodification()方法中。不忙看checkForComodification()方法的具体实现,先根据程序的代码一步一步看ArrayList源码的实现。

    二、foreach原理

    直接看结论即可

    首先、追究foreach的原理,暂时删除其他的遍历方法,只保留foreach的写法:

    public class Test {
    
        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
            list.add("a");
            list.add("b");
            list.add("b");
            list.add("b");
            // foreach循环
            for (String str : list) {
                if (str.equals("b")) {
                    list.remove(str);
                }
            }
        }
    }

    编译后的.class文件(eclipse 直接打开可以查看),截取其中for循环的部分:

    44  aload_1 [list]
    45  invokeinterface java.util.List.iterator() : java.util.Iterator [29] [nargs: 1]
    50  astore_3
    51  goto 81
    54  aload_3
    55  invokeinterface java.util.Iterator.next() : java.lang.Object [33] [nargs: 1]
    60  checkcast java.lang.String [39]
    63  astore_2 [str]
    64  aload_2 [str]
    65  ldc <String "b"> [27]
    67  invokevirtual java.lang.String.equals(java.lang.Object) : boolean [41]
    70  ifeq 81
    73  aload_1 [list]
    74  aload_2 [str]
    75  invokeinterface java.util.List.remove(java.lang.Object) : boolean [44] [nargs: 2]
    80  pop
    81  aload_3
    82  invokeinterface java.util.Iterator.hasNext() : boolean [47] [nargs: 1]
    87  ifne 54

    第45行:调用List中的list.iterator()方法,获取集合的迭代器Iterator对象。
    第51行:注意,goto 81,因此是调用第81、82行的hasNext()方法。
    第55行:调用next方法,获取第一个list中第一个元素:String字符串。
    第67行:调用String的equals方法比较。
    第75行:注意,此时remove方法仍然是list的方法,而不是迭代器的remove。
    第82行:调用迭代器的hasNext()方法,判断是否继续遍历。


    经过整理、优化,foreach的底层代码可以使用下方的代码替换:

    public void test1() {
        ArrayList<String> list = new ArrayList<String>();
        list.add("b");
        list.add("b");
        list.add("b");
        Iterator<String> iterator = list.iterator();//获取迭代器
        while (iterator.hasNext()) {//继续循环
            String value = iterator.next();//获取遍历到的值
            if (value.equals("b")) {
                list.remove(value);//list的remove
            }
        }
    }

    结论:

    1、遍历集合的增强for循环最终都是使用的Iterator迭代器

    2、集合的remove(add)方法却仍然调用list的方法,而不是Iterator的方法。

    不使用迭代器和不使用增强for循环是不会引起ConcurrentModificationException的,参看单线程解决方案3.不使用Iterator进行遍历(即使用for ( int i = 0; i < myList.size(); i++)形式)

    三、从ArrayList源码找原因

    跟进ArrayList的源码看, 搜索iterator()方法看其获得的迭代器, 发现没有!  于是追其父类 AbstractList,  iterator()方法返回new Itr()!

    查看Itr中的两个重要的方法:  hasNext与next

            public boolean hasNext() {
                return cursor != size();
        }
     
        public E next() {
                checkForComodification();
            try {
            E next = get(cursor);
            lastRet = cursor++;
            return next;
            } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
            }
        }

    看next中调用的checkForComodification(), 在remove方法中也调用了checkForComodification()!接着checkForComodification()方法里面在做些什么事情!

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

    所以在迭代的过程中,hasNext()是不会抛出ConcurrentModificationException的, next和remove可能方法会抛!  抛异常的标准就是modCount != expectedModCount!继续跟踪这两个变量,在Itr类的成员变量里对expectedModCount初始化的赋值是int expectedModCount = modCount;
    那么这个modCount呢.? 这个是AbstractList中的一个protected的变量,  在对集合增删的操作中均对modCount做了修改,  因为这里是拿ArrayList为例, 所以直接看ArrayList中有没有覆盖父类的add?   结果发现覆盖了

    public boolean add(E e) {
        ensureCapacity(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
        }
     
    public void ensureCapacity(int minCapacity) {
        modCount++;
        int oldCapacity = elementData.length;
        if (minCapacity > oldCapacity) {
            Object oldData[] = elementData;
            int newCapacity = (oldCapacity * 3)/2 + 1;
                if (newCapacity < minCapacity)
            newCapacity = minCapacity;
                // minCapacity is usually close to size, so this is a win:
                elementData = Arrays.copyOf(elementData, newCapacity);
        }
        }

    remove方法中也做了modCount++,  

    ArrayList中的remove做的事情:

    public boolean remove(Object paramObject) {
        int i;
        if (paramObject == null) {
            for (i = 0; i < size; i++) {
                if (elementData[i] == null) {
                    fastRemove(i);
                    return true;
                }
            }
        } else {
            for (i = 0; i < size; i++) {
                if (paramObject.equals(elementData[i])) {
                    fastRemove(i);
                    return true;
                }
            }
        }
        return false;
    }
     
    private void fastRemove(int paramInt) {
        modCount += 1;
        int i = size - paramInt - 1;
        if (i > 0) {
            System.arraycopy(elementData, paramInt + 1, elementData, paramInt, i);
        }
        elementData[(--size)] = null;
    }

    当我获得迭代器之前, 无论对集合做了多少次添加删除操作, 都没有关系, 因为对expectedModCount赋值是在获取迭代器的时候初始化的.

    关键点就在于:调用list.remove()或list.add()方法导致modCount和expectedModCount的值不一致。

    四、单线程解决方案

    1、对于没有使用foreach循环,代码里使用了迭代器的程序,可以把list.remove(value);替换为:iterator.remove();

    看下 iterator.remove();的具体实现:

    public void remove() {
        if (lastRet < 0) {
            throw new IllegalStateException();
        }
        checkForComodification();
        try {
            remove(lastRet);
            if (lastRet < cursor) {
                cursor -= 1;
            }
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException localIndexOutOfBoundsException) {
            throw new ConcurrentModificationException();
        }
    }

    iterator.remove();相比list.remove(value);多了一步expectedModCount = modCount; 此时保证了checkForComodification()方法检查通过。

    代码改成如下所示:

    public void test1() {
        List<String> list = new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("b");
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String value = it.next();
            if (value.equals("b")) {
                // list.remove(value);
                it.remove();
            }
        }
    }

    2、使用临时的集合,把需要remove的元素保存在临时的集合中,最后再把临时集合一起remove掉。

    public void test2() {
        List<String> list = new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("b");
        // 临时的list_add
        List<String> list_add = new ArrayList<String>();
        for (String str : list) {
            if (str.equals("b")) {
                list_add.add(str);
            }
        }
        list.removeAll(list_add);//最后统一移除
        System.out.println(list);
    }

    3.不使用Iterator进行遍历,即使用for ( int i = 0; i < myList.size(); i++)形式。需要注意的是自己保证索引正常

        for ( int i = 0; i < myList.size(); i++) {
                    String value = myList.get(i);
                    System. out.println( "List Value:" + value);
                     if (value.equals( "3")) {
                         myList.remove(value);  // ok
                         i--; // 因为位置发生改变,所以必须修改i的位置
                    }
               }
               System. out.println( "List Value:" + myList.toString());

    五、在多线程环境下的解决方法

    下面的例子中开启两个子线程,一个进行遍历,另外一个有条件删除元素:

         final List myList = createTestData();
    
              new Thread(new Runnable() {
    
                   @Override
                   public void run() {
                        for (String string : myList) {
                             System.out.println("遍历集合 value = " + string);
    
                             try {
                                  Thread.sleep(100);
                             } catch (InterruptedException e) {
                                  e.printStackTrace();
                             }
                        }
                   }
              }).start();
    
              new Thread(new Runnable() {
    
                   @Override
                   public void run() {
    
                     for (Iterator it = myList.iterator(); it.hasNext();) {
                         String value = it.next();
    
                         System.out.println("删除元素 value = " + value);
    
                         if (value.equals( "3")) {
                              it.remove();
                         }
    
                         try {
                                  Thread.sleep(100);
                             } catch (InterruptedException e) {
                                  e.printStackTrace();
                             }
                    }
                   }
              }).start();
    Exception in thread "Thread-0" 删除元素 value = 4
    java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
    at java.util.AbstractList$Itr.next(Unknown Source)
    at list.ConcurrentModificationExceptionStudy$1.run(ConcurrentModificationExceptionStudy.java:42)
    at java.lang.Thread.run(Unknown Source)
    删除元素 value = 5

    有可能有朋友说ArrayList是非线程安全的容器,换成Vector就没问题了,实际上换成Vector还是会出现这种错误。

      原因在于,虽然Vector的方法采用了synchronized进行了同步,但是实际上通过Iterator访问的情况下,每个线程里面返回的是不同的iterator,也即是说expectedModCount是每个线程私有。假若此时有2个线程,线程1在进行遍历,线程2在进行修改,那么很有可能导致线程2修改后导致Vector中的modCount自增了,线程2的expectedModCount也自增了,但是线程1的expectedModCount没有自增,此时线程1遍历时就会出现expectedModCount不等于modCount的情况了。

    结论: 
    上面的例子在多线程情况下,使用it.remove(),
    说明使用it.remove()的办法在同一个线程执行的时候是没问题的,但是在多线程进行迭代情况下依然可能出现异常

     参看iterator.remove();的具体实现,如果在iterator.remove()的expectedModCount = modCount;之前线程切换,则另一个线程检查expectedModCount和modCount不一致,抛ConcurrentModificationException异常

    解决方案 :

    1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;

    2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。

    CopyOnWriteArrayList注意事项
    (1) CopyOnWriteArrayList不能使用Iterator.remove()进行删除。
    (2) CopyOnWriteArrayList使用Iterator且使用List.remove(Object);会出现如下异常:

    java.lang.UnsupportedOperationException: Unsupported operation remove
    at java.util.concurrent.CopyOnWriteArrayList$ListIteratorImpl.remove(CopyOnWriteArrayList.java:804)

    原因是CopyOnWriteArrayList的阉割版迭代器COWIterator源码中

    static final class COWIterator<E> implements ListIterator<E> {
    public void remove() {
                throw new UnsupportedOperationException();
            }
    //省略
    }

     六.Java快速失败(fail-fast)和安全失败(fail-safe)

    当错误发生时,如果系统立即关闭,即是快速失败,系统不会继续运行。运行中发生错误,它会立即停止操作,错误也会立即暴露。而安全失败系统在错误发生时不会停止运行。它们隐蔽错误,继续运行,而不会暴露错误。这两种模式,孰优孰优,是系统设计中常讨论的话题,在此,我们只讨论java中的快速失败和安全失败迭代器。

    Java快速失败与安全失败迭代器 :

    java迭代器提供了遍历集合对象的功能,集合返回的迭代器有快速失败型的也有安全失败型的,快速失败迭代器在迭代时如果集合类被修改,立即抛出ConcurrentModificationException异常,而安全失败迭代器不会抛出异常,因为它是在集合类的克隆对象上操作的。我们来看看快速失败和 安全失败迭代器的具体细节。

    java快速失败迭代器 :

    大多数集合类返回的快速失败迭代器在遍历时不允许结构性修改(结构性修改指添加,删除) 当遍历的同时被结构性修改,就会抛出ConcurrentModificationException异常,而当集合是被迭代器自带的方法(如remove())修改时,不会抛出异常。

    Java安全失败迭代器 :

    安全失败迭代器在迭代中被修改,不会抛出任何异常,因为它是在集合的克隆对象迭代的,所以任何对原集合对象的结构性修改都会被迭代器忽略,但是这类迭代器有一些缺点,其一是它不能保证你迭代时获取的是最新数据,因为迭代器创建之后对集合的任何修改都不会在该迭代器中更新。

        

    java.util包下的集合类都是快速失败的,java.util.concurrent包下的容器都是安全失败如ConcurrentHashMap

     ConcurrentHashMap迭代器复制了一份map:

        static class BaseIterator<K,V> extends Traverser<K,V> {
            final ConcurrentHashMap<K,V> map;
            Node<K,V> lastReturned;
            BaseIterator(Node<K,V>[] tab, int size, int index, int limit,
                        ConcurrentHashMap<K,V> map) {
                super(tab, size, index, limit);
                this.map = map;
                advance();
            }

    https://blog.csdn.net/shaohe18362202126/article/details/83795991

    https://blog.csdn.net/qq_30051139/article/details/54019515?utm_source=blogxgwz3

    https://blog.csdn.net/izard999/article/details/6708738

    https://www.cnblogs.com/dolphin0520/p/3933551.html

  • 相关阅读:
    HTTP 深入详解(HTTP Web 的基础)
    webpack 代码分离
    webpack 常见问题
    细说 webpack 之流程篇
    一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?
    Ajax 解决浏览器缓存问题
    十大经典排序算法
    react-redux 之 connect 方法详解
    JS实现继承的几种方式
    GIT常用命令及常见问题解决方法-协作篇
  • 原文地址:https://www.cnblogs.com/twoheads/p/9843055.html
Copyright © 2011-2022 走看看