1. 程式人生 > 其它 >java併發修改異常

java併發修改異常

技術標籤:java學習筆記java

迭代器是依賴於集合而存在的,在判斷成功後,集合的中新添加了元素,而迭代器卻不知道,所以就報錯了,這個錯叫併發修改異常。

一:問題程式碼:


import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Test {
    public static void main(String[] args) {
       Collection<String> c=new ArrayList<String>();
       c.add("a");
       c.add("b");
       c.add("c");
       
        Iterator<String> itr=c.iterator();
        
        while(itr.hasNext()){   //迭代器遍歷,但是下面卻用了集合的remove方法
            String str= (String)itr.next();
            if(str.equals("a"))
                c.remove("b");
        }
    }
}

報錯資訊如下:

二:問題分析

問題出現場景:ConcurrentModificationException:當方法檢測到物件的併發修改,但不允許這種修改時,丟擲此異常。

換句話說就是:迭代器遍歷的時候只能用迭代器的remove()函式,如果用集合物件的remove函式就會爆出併發修改異常。

我們選擇一步一步檢視原始碼:我們想著直接查checkForComodification()函式,但是不行。所以我們先找到ArrayList,然而ArrayList類中根本沒有迭代器的實現,所以我猜測Iterator大概是在他的父類或者介面中。

不出所料:在AbstractList類內果然發現了迭代器的實現函式,程式碼很簡短,從這段程式碼可以看出返回的是一個指向Itr型別物件的引用,但是又出現了我們不認識的Itr(),所以接著去找

//下面是AbstractList的程式碼,只保留了內部類Itr和部分有用的程式碼。下面會拆開詳細講
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

        public Iterator<E> iterator() {
            return new Itr();
        }

        //可以看出Itr就是AbstractList的一個成員內部類
        private class Itr implements Iterator<E> {
            int expectedModCount = modCount;

            public boolean hasNext() {
                return cursor != size();
            }

            public E next() {
                checkForComodification();
                try {
                    int i = cursor;
                    E next = get(i);
                    lastRet = i;
                    cursor = i + 1;
                    return next;
                } catch (IndexOutOfBoundsException e) {
                    checkForComodification();
                    throw new NoSuchElementException(e);
                }
            }

            public void remove() {
                if (lastRet < 0)
                    throw new IllegalStateException();
                checkForComodification();

                try {
                    AbstractList.this.remove(lastRet);
                    if (lastRet < cursor)
                        cursor--;
                    lastRet = -1;
                    expectedModCount = modCount;
                } catch (IndexOutOfBoundsException e) {
                    throw new ConcurrentModificationException();
                }
            }
//這就是最開始報錯資訊類的那個”罪魁禍首“函式,可以看出它的作用就是判斷兩個變數是否相等,那這個兩個變數是什麼呢?下面馬上就講。
            final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
        }

這裡先講兩個變數:

expectedModCount:這個變數是AbstractList裡面的內部類ltr中的變數。代表預期的修改次數,通過public Iterator<E> iterator() { return new Itr();}這個函式的時候會自動new一個Itr物件,同時會用modCount為其初始化。

int expectedModCount = modCount;

modCount:這個是AbstractList類中的變數。表示實際的修改次數。

protected transient int modCount = 0;

該值表示對List的修改次數,檢視ArrayList的add()和remove()方法就可以發現,每次呼叫add()方法或者remove()方法就會對modCount進行加1操作。

好了,到這裡我們再回到最初的程式:

  當呼叫c.iterator()返回一個Iterator之後,通過Iterator的hashNext()方法判斷是否還有元素未被訪問,我們看一下hasNext()方法,hashNext()方法的實現很簡單:

public boolean hasNext() {
    return cursor != size();
}

如果下一個訪問的元素下標不等於ArrayList的大小,就表示有元素需要訪問,這個很容易理解,如果下一個訪問元素的下標等於ArrayList的大小,則肯定到達末尾了。

然後通過Iterator的next()方法獲取到下標為0的元素,我們看一下next()方法的具體實現:

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

這裡是非常關鍵的地方:首先在next()方法中會呼叫checkForComodification()方法,然後根據cursor的值獲取到元素,接著將cursor的值賦給lastRet,並對cursor的值進行加1操作。

初始時,cursor為0,lastRet為-1,那麼呼叫一次之後,cursor的值為1,lastRet的值為0。

注意此時,modCount為0,expectedModCount也為0。

 接著往下看上面的出錯程式,程式中判斷當前元素的值是否為a,若為a,則呼叫物件中的remove()方法(而不是迭代器中的remove函式)來刪除該元素。

   那我們先看一下在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
}

通過remove方法刪除元素最終是呼叫的fastRemove()方法,在fastRemove()方法中,首先對modCount進行加1操作(因為對集合修改了一次),然後接下來就是刪除元素的操作,最後將size進行減1操作,並將引用置為null以方便垃圾收集器進行回收工作。

  那麼注意此時各個變數的值:對於物件c的迭代器itr,其expectedModCount為0,cursor的值為1,lastRet的值為0。

  對於物件c本身,其modCount為1,size為0。

  接著看程式程式碼,執行完相應操作後,經過判斷繼續while迴圈,呼叫hasNext方法()判斷,然後繼續呼叫迭代器itr的next()方法:

  注意,此時要注意next()方法中的第一句:checkForComodification()。

  在checkForComodification方法中進行的操作是:

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

 很顯然,此時modCount為1,而expectedModCount為0,modCount不等於expectedModCount,因此程式就丟擲了ConcurrentModificationException異常。

錯誤是在第一次while迴圈remove函式呼叫完埋下的,是第二次while迴圈next()函式時爆出來的。

  關鍵點就在於:呼叫list.remove()方法導致modCount和expectedModCount的值不一致。也就是AbstractList的內部類中的Itr內部類只初始化一次,如果初始化之後再跳出內部類進行其他操作的話,modCount的值就可能改變,但是exceptedModCount的值卻無法同步改變了。這就出錯了,而且併發修改異常也就很容易理解為什麼要這樣叫了。

  注意,像使用for-each進行迭代實際上也會出現這種問題。

三:問題解決。

1:單執行緒下

上面以及說過了:迭代器遍歷的時候用集合的函式修改就會丟擲併發修改異常。

修改方法1.迭代器遍歷,呼叫迭代器函式進行操作。

      Iterator<String> itr=c.iterator();
//1.迭代器遍歷,呼叫迭代器的操作函式
       while(itr.hasNext()){
            String str= (String)itr.next();
            if(str.equals("a"))
                itr.remove();
        }

修改方法2.傳統的物件for迴圈遍歷,呼叫物件函式進行操作。

     //2.選用普通的for迴圈遍歷,呼叫物件的操作函式
       for(int i=0;i<c.size();i++){
           //這裡必須進行一個強制的型別轉換,取出來的是Object型別的物件,你得轉換成String
              String str= ((ArrayList<String>) c).get(i);
              if(str.equals("a"))
                  c.remove("a");  
 //一個小細節,iterator裡的remove函式不需要傳參,物件的需要傳參
       }

總之就是兩條線路各走各的,不能交叉。

2:多執行緒下

多執行緒下的見下面的參考網址:https://www.cnblogs.com/bsjl/p/7676209.html