集合之併發修改異常(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併發修改異常。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章