1. 程式人生 > >Java併發-ConcurrentModificationException原因原始碼分析與解決辦法

Java併發-ConcurrentModificationException原因原始碼分析與解決辦法

一、異常原因與異常原始碼分析

  對集合(List、Set、Map)迭代時對其進行修改就會出現java.util.ConcurrentModificationException異常。這裡以ArrayList為例,例如下面的程式碼:

ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
//遍歷1
for (String s : list){
    if (s.equals( "3")) {
        list.remove(s);  // error
} } //遍歷2 Iterator<String> it = list.iterator(); for (; it.hasNext();) { String value = it.next(); if (value.equals("3")) { list.remove(value); // error } }

 ArrayList類中包含了實現Iterator迭代器的內部類Itr,在Itr類內部維護了一個expectedModCount變數,而在ArrayList類中維護一個modCount變數(modCount是ArrayList實現AbstractList類得到成員變數)。其他集合(List、Set、Map)都與之類似。

  當對集合進行新增或者刪除操作時modCount的值都會進行modCount++操作,例如ArrayList中的remove()方法:

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; // Let gc do its work }

  當集合新增完值後,對集合進行遍歷時才會建立Itr物件,這時候會執行int expectedModCount = modCount;操作,也就是說只要是在增加或刪除後對集合進行遍歷,那expectedModCount 與modCount永遠是相等的。

  但是如果在遍歷的過程中進行增加或刪除操作那麼modCount++,但是expectedModCount儲存的還是遍歷前的值,也就是expectedModCount和modCount的值是不相等的。

  遍歷過程中會呼叫iterator的next()方法,next()方法方法會首先呼叫checkForComodification()方法來驗證expectedModCount和modCount是否相等,因為之前做了增加或刪除操作,modCount的值發生了變化,所以expectedModCount和modCount不相等,丟擲ConcurrentModificationException異常。

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() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

二、單執行緒解決方案

1、迭代器刪除

  在Itr類中也給出了一個remove()方法,通過呼叫Itr類的方法就可以實現而且不報錯,例如下面程式碼:

ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.remove("4");
//遍歷2
Iterator<String> it = list.iterator();
for (; it.hasNext();) {
    String value =  it.next();
    if (value.equals("3")) {
        it.remove();  
    }
}

  在Itr類中remove()方法中,執行了expectedModCount = modCount操作,那麼執行next()方法時expectedModCount和modCount肯定相等,Itr類中remove()方法的原始碼:

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

2、其他的方式

 // 2 建一個集合,記錄需要刪除的元素,之後統一刪除             
List<string> templist = new ArrayList<string>();
 for (String value : myList) {
      if (value.equals( "3")) {
          templist.remove(value);
     }
}
 // 可以檢視removeAll原始碼,其中使用Iterator進行遍歷
myList.removeAll(templist);
System. out.println( "List Value:" + myList.toString());        
 
  // 3. 使用執行緒安全CopyOnWriteArrayList進行刪除操作
List<string> myList = new CopyOnWriteArrayList<string>();
myList.add( "1");
myList.add( "2");
myList.add( "3");
myList.add( "4");
myList.add( "5");
 
Iterator<string> it = myList.iterator();
 
 while (it.hasNext()) {
     String value = it.next();
      if (value.equals( "3")) {
          myList.remove( "4");
          myList.add( "6");
          myList.add( "7");
     }
}
System. out.println( "List Value:" + myList.toString());
 
 // 4. 不使用Iterator進行遍歷,需要注意的是自己保證索引正常
 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的位置
     }
}

三、多執行緒解決方案

1、多執行緒下異常原因

  多執行緒下ArrayLis用Itr類中remove()方法也是會報異常的,Vector(執行緒安全)也會出現這種錯誤,具體原因如下:

  Itr是在遍歷的時候建立的,也就是每個執行緒如果遍歷都會得到一個expectedModCount ,expectedModCount 也就是每個執行緒私有的,假若此時有2個執行緒,執行緒1在進行遍歷,執行緒2在進行修改,那麼很有可能導致執行緒2修改後導致Vector中的modCount自增了,執行緒2的expectedModCount也自增了,但是執行緒1的expectedModCount沒有自增,此時執行緒1遍歷時就會出現expectedModCount不等於modCount的情況了。

2、嘗試方案

(1) 在所有遍歷增刪地方都加上synchronized或者使用Collections.synchronizedList,雖然能解決問題但是並不推薦,因為增刪造成的同步鎖可能會阻塞遍歷操作。
(2) 推薦使用ConcurrentHashMap或者CopyOnWriteArrayList。

3、CopyOnWriteArrayList使用注意

(1) CopyOnWriteArrayList不能使用Iterator.remove()進行刪除。
(2) CopyOnWriteArrayList使用Iterator且使用List.remove(Object);會出現如下異常:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");

Iterator<String> it = list.iterator();
for (; it.hasNext();) {
    String value =  it.next();
    if (value.equals("4")) {
        it.remove();  // error
    }
}

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1040)
    at TestZzl.main(TestZzl.java:51)

4、最終解決方案

List<string> myList = new CopyOnWriteArrayList<string>();
 myList.add( "1");
 myList.add( "2");
 myList.add( "3");
 myList.add( "4");
 myList.add( "5");
 
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 (int i = 0; i < myList.size(); i++) {
               String value = myList.get(i);
             
               System.out.println("刪除元素 value = " + value);
         
           if (value.equals( "3")) {
                myList.remove(value);
                i--; // 注意                           
           }
           try {
                    Thread.sleep(100);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
          }
     }
}).start();

後續會具體分析一下CopyOnWriteArrayList

參考:

https://www.2cto.com/kf/201403/286536.html

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