集合之并发修改异常(ConcurrentModificationException)

tips:集合之并发修改异常(ConcurrentModificationException)

1-背景

  • 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException。

2-原理

  • 迭代器在遍历时会直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
代码: 增强foreach遍历集合
        List<Integer> list = new ArrayList<>();
        list.add(12);
        list.add(13);
        list.add(14);
        for(Integer value:list){
           if(value.intValue()==13){
               list.remove(value);
           }
        }
上述代码: java for增强遍历集合 会重写方法等同于iterator遍历集合
        List<Integer> list = new ArrayList<>();
        list.add(12);
        list.add(13);
        list.add(14);
        Iterator<Integer> it = list.iterator();
        while(it.hasNext()){
            // 抛ConcurrentModificationException异常的点: it.next()方法 分析源码
            Integer value = it.next();
            if(value.intValue()==12){
                list.remove(value);
            }
        }
分析it.next()此处实现源码 【代码位于ArrayList源码的内部类里面】
        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();
            }
        }
        // 异常抛出最终位置 【条件: modCount != expectedModCount】
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
追溯变量modCount 与 expectedModCount
  • 查看list方法的源码,发现集合list每次操作add,remove,clear都会增加modCount值
        public E remove(int index) {
          ...
          modCount++;
          ...
      }
    
  • 而expectedModCount值在ArrayList的内部类Itr中,在声明初始化时候会将modCount赋值给它
          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;
    
  • 所以在上述foreach或者iterator遍历集合时,针对集合list.remove(12)操作后,此时modCount++,expectedModCount值保持不变,所以iterator.next()指向下一个元素时候检查并抛出该异常。
    • 核心代码:
          Iterator<Integer> it = list.iterator();
          while(it.hasNext()){
              // 抛ConcurrentModificationException异常的点: it.next()方法 分析源码
              Integer value = it.next();
              if(value.intValue()==12){
                  list.remove(value);
              }
          }
    

ArrayList内部类的remove()方法
  • 解决方法
    • 单线程scenes
      Iterator<Integer> iterator2 = list.iterator();
      while(iterator2.hasNext()){
          Integer value = iterator2.next();
          if(value.intValue()==13){
              iterator2.remove();
          }
      }
    
    • 解释:上面看到ArrayList的内部类有remove()方法,首先调用ArrayList.remove(index)方法,接着进行赋值操作expectedModCount = modCount 即可保证两者相等
      • 核心代码
          ...
          ArrayList.this.remove(lastRet);
          ...
          expectedModCount = modCount;
      
    • 多线程scenes
      • 如果两个线程没有同步锁或者其他措施,也容易产生上述异常,因为一个线程操作时候更改了modCount值,而另外一个迭代时该迭代器的expectedModCount与modCOunt值不一致。解决方法如下:
      • 01 迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
          new Thread(
              ()->{
                  synchronized (list){
                      Iterator<Integer> it01 = list.iterator();
                      while(it01.hasNext()){
                          System.out.println(Thread.currentThread().getName()+" "+it01.next());
                          try {
                              Thread.sleep(1);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  }
      
              }
          ).start();
          new Thread(()->{
              synchronized (list){
                  Iterator<Integer> it02 = list.iterator();
                  while(it02.hasNext()){
                      System.out.println(Thread.currentThread().getName()+" "+it02.next());
                      if(it02.next().intValue()==14){
                          it02.remove();
                      }
                  }
              }
            }
          ).start();
      
      • 02 采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作;原理:CopyOnWriteArrayList也是一个线程安全的ArrayList,其实现在于每次add,remove等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。
      // 核心代码
      static List<String> list = new CopyOnWriteArrayList<String>();
      
      public static void main(String[] args) {
          list.add("a");
          list.add("b");
          list.add("c");
          list.add("d");
      
          new Thread() {
              public void run() {
                  Iterator<String> iterator = list.iterator();
      
                      while (iterator.hasNext()) {
                          System.out.println(Thread.currentThread().getName()
                                  + ":" + iterator.next());
                          try {
                              Thread.sleep(1000);
                          } catch (InterruptedException e) {
                              // TODO Auto-generated catch block
                              e.printStackTrace();
                          }
                      }
              };
          }.start();
      
          new Thread() {
              public synchronized void run() {
                  Iterator<String> iterator = list.iterator();
      
                      while (iterator.hasNext()) {
                          String element = iterator.next();
                          System.out.println(Thread.currentThread().getName()
                                  + ":" + element);
                          if (element.equals("c")) {
                              list.remove(element);
                          }
                      }
              };
          }.start();
      
      }
      

3-疑惑

  • 1.既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
    • 解释:
      • fast-fail与fail-safe
      • 参考2
      • 源码内部有错误检测机制:核心fail-fast即用来检查多线程对线程进行操作造成并发问题。
      • ConcurrentModificationException并发修改异常。

你可能感兴趣的:(java,集合)