併發修改異常ConcurrentModificationException詳解

目錄

一、簡介

二、異常原因分析

三、異常原因追蹤

四、如何避免併發修改異常?

五、總結

一、簡介

在多線程編程中,相信很多小夥伴都遇到過併發修改異常ConcurrentModificationException,本篇文章我們就來講解併發修改異常的現象以及分析一下它是如何產生的。

  • 異常產生原因:併發修改異常指的是在併發環境下,當方法檢測到對象的併發修改,但不允許這種修改時,拋出該異常。

下面看一個示例:

public class TestConcurrentModifyException {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String nextElement = iterator.next();
            if (Integer.parseInt(nextElement) < 2) {
                list.add("2");
            }
        }
    }
}

運行此程序,控制檯輸出,程序出現異常:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.wsh.springboot.helloworld.TestConcurrentModifyException.main(TestConcurrentModifyException.java:15)

可見,控制檯顯示的ConcurrentModificationException,即併發修改異常。下面我們就以ArrayList集合中出現的併發修改異常爲例來分析異常產生的原因。

二、異常原因分析

通過上面的異常信息可見異常拋出在ArrayList類中的checkForComodification()方法中。下面是checkForComodification方法的源碼: 

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

checkForComodification()方法實際上就是當modCount 變量值不等於expectedModCount變量值時,就會觸發此異常。

那麼modCount 和expectedModCount分別代表什麼呢?

  • modCount :AbstractList類中的一個成員變量,由於ArrayList繼承自AbstractList,所以ArrayList中的modCount變量也繼承過來了。
protected transient int modCount = 0;

簡單理解,modCount 就是ArrayList中集合結構的修改次數【實際修改次數】,指的是新增、刪除(不包括修改)操作

  • expectedModCount:是ArrayList中內部類Itr的一個成員變量,當我們調用iteroter()獲取迭代器方法時,會創建內部類Itr的對象,並給其成員變量expectedModCount賦值爲ArrayList對象成員變量的值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
    //將ArrayList對象成員變量的值modCount賦值給expectedModCount成員變量
    int expectedModCount = modCount;
    //....
    }

經過上面的分析,我們知道了當我們獲取到集合的迭代器之後,Itr對象創建成功後,expectedModCount 的值就確定了,就是modCount的值,在迭代期間不允許改變了。要了解它兩爲啥不相等, 我們就需要觀察ArrayList集合的什麼操作會導致modCount變量發生變化,從而導致modCount != expectedModCount ,從而發生併發修改異常。

查看ArrayList的源碼可知,modCount 初始值爲0, 每當集合中添加一個元素或者刪除一個元素時,modCount變量的值都會加一,表示集合中結構修改次數多了一次。下面簡單看下ArrayList的add()方法和remove()方法。

  • add():每添加一個元素,modCount的值也會自增一次
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    //第一次添加元素
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //默認容量DEFAULT_CAPACITY爲10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    //集合結構修改次數加一
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        //擴容方法,擴容後是原容量的1.5倍
        //擴容前:數組長度10  擴容後:數組長度變爲10 + (10 / 2) = 15
        grow(minCapacity);
}
  • remove():每刪除一個元素,modCount的值會自增一次
public E remove(int index) {
    //檢查索引是否越界
    rangeCheck(index);
    
    //集合結構修改次數加一
    modCount++;
    //數組中對應索引的值
    E oldValue = elementData(index);

    //計算需要移動元素的位數
    int numMoved = size - index - 1;
    if (numMoved > 0)
        //數組拷貝
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //將元素置空,利於垃圾回收
    elementData[--size] = null; // clear to let GC do its work
    //返回原先索引對應的值
    return oldValue;
}

注意!注意!注意!ArrayList中的修改方法set()並不會導致modCount變量發生變化,set()方法源碼如下:

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

三、異常原因追蹤

 下面我們就Debug調試一下剛剛那個例子,詳解了解一下,併發修改異常時怎麼產生的。

當我們調用iterator()獲取迭代器時,實際上底層創建了一個Itr內部類對象

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

初始化Itr的成員變量:可以看到,expectedModCount = 3,表示預期修改次數爲3,如果在迭代過程中,發現modCount不等於3了,那麼就會觸發併發修改異常。

下面簡單說明一下Itr的源碼:

private class Itr implements Iterator<E> {
    //cursor初始值爲0,每次取出一個元素,cursor值會+1,以便下一次能指向下一個元素,直到cursor值等於集合的長度爲止
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    //初始化預期修改次數爲實際修改次數modCount,即上圖中的3
    int expectedModCount = modCount;

    //判斷是否還有下一個元素:通過比較遊標cursor是否等於數組的長度
    //因爲集合中最後一個元素的索引爲size-1,只要cursor值不等於size,證明還有下一個元素,此時hasNext方法返回true,
   //如果cursor值與size相等,那麼證明已經迭代到最後一個元素,返回false
    public boolean hasNext() {
        return cursor != size;
    }

    //拿出集合中的下一個元素
    @SuppressWarnings("unchecked")
    public E next() {
        //併發修改異常出現根源
        //ConcurrentModificationException異常就是從這拋出的
        //當迭代器通過next()方法返回元素之前都會檢查集合中的modCount和最初賦值給迭代器的expectedModCount是否相等,如果不等,則拋出併發修改異常
        checkForComodification();
        int i = cursor;
        //判斷,如果大於集合的長度,說明沒有元素了。
        if (i >= size)
            throw new NoSuchElementException();
        //將集合存儲數據數組的地址賦值給局部變量elementData     
        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();
        }
    }
    
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

繼續Debug,我們記錄一下幾次hasNext()/next()方法時,其中幾個重要變量值的變化過程。

第一次調用hasNext(): cursor = 0    size = 3
第一次調用iterator.next():
    第一次調用checkForComodification():modCount = 3  expectedModCount = 3
由於 modCount = expectedModCount ,不會發生併發修改異常。並且返回當前遊標對應的值,即返回1.
由於滿足Integer.parseInt(nextElement) < 2,所以會執行list.add("2")方法,之前已經瞭解到,add()方法會
修改modCount的值 + 1· 所以此時modCount的值變爲4了.
第一次next()方法調用完,cursor遊標的值會加一,所以cursor = 1. 

===============================================================================================================
第二次調用hasNext(): cursor = 1  size = 4
第二次調用iterator.next():
    第二次調用checkForComodification():modCount = 4  expectedModCount = 3
由於 modCount != expectedModCount ,此時會發生併發修改異常。

以上就是ConcurrentModificationException一場產生的簡單解析過程。  

下圖是發生併發修改異常時checkForComodification()方法的執行過程,注意modCount和expectedModCount 的值:

四、併發修改異常的特殊情況

示例:已知集合中有三個元素:"chinese"、"math"、"english",使用迭代器進行遍歷, 判斷集合中存在"english",如果存在則刪除。

public class TestConcurrentModifyException {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("chinese");
        list.add("math");
        list.add("english");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String nextElement = iterator.next();
            if ("english".equals(nextElement)) {
                //使用ArrayList的boolean remove(Object o)方法進行刪除
                list.remove("english");
            }
        }
    }
}

程序運行結果:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.wsh.springboot.helloworld.TestConcurrentModifyException.main(TestConcurrentModifyException.java:19)

通過上面的分析,由於往集合中加入了三個元素,所以modCount實際修改次數的值爲3,當我們調用iterator()獲取迭代器的時候,初始化expectedModCount的值也爲3。下面我們一起看一下ArrayList類中的根據元素刪除方法的源碼。

remove(Object o)方法源碼: 

public boolean remove(Object o) {
    //判斷需要刪除的元素是否爲null
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        //不爲null,遍歷集合,使用equals進行比較是否相等
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    //刪除元素時,實際修改次數會自增1
    //此時: modCount實際修改次數爲4,但是預期修改次數還是獲取迭代器時候的3,兩者已經不一致了。
    modCount++;
    //計算集合需要移動元素的個數
    int numMoved = size - index - 1;
    if (numMoved > 0)
        //數組拷貝
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //將刪除元素置爲null,利於垃圾回收
    elementData[--size] = null; // clear to let GC do its work
}

我們分析一下程序的執行過程,查看併發修改異常是怎麼產生的。當我們執行到下面一行語句之後,集合的size會減1,所以此時size = 2.

list.remove("english");

那麼這時候再次執行下面的判斷

while (iterator.hasNext()) {

此時cursor的值是3,但是size的值是2,兩者不相等,所以hasNext()方法返回true,意味着集合中還有元素,所以還會執行一次next()方法,此時執行checkForComodification()方法,判斷modCount是否等於expectedModCount,(expectedModCount=3, modCount=4),兩者不相等,所以這就拋出了併發修改異常。

小結論:

  • 1.集合每次調用add方法時,實際修改次數的值modCount都會自增1;
  • 2.在獲取迭代器的時候,集合只會執行一次將實際修改集合的次數modCount的值賦值給預期修改的次數變量expectedModCount;
  • 3.集合在刪除元素的時候,也會針對實際修改次數modCount的變量進行自增操作; 

下面再來看一個併發修改異常的特殊情況,觀察下面的程序:

示例:已知集合中有三個元素:"chinese"、"math"、"english",使用迭代器進行遍歷,判斷集合中存在"math",如果存在則刪除。

public class TestConcurrentModifyException {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("chinese");
        list.add("math");
        list.add("english");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String nextElement = iterator.next();
            if ("math".equals(nextElement)) {
                //使用ArrayList的boolean remove(Object o)方法進行刪除
                list.remove("math");
            }
        }
        System.out.println(list);
    }
}

我們可以看到,這個示例跟上面一個實例非常相似,唯一不同的是這次刪除的元素是集合中的倒數第二個元素。

程序運行結果:

[chinese, english]

我們看到,這裏並沒有發生併發修改異常,很神奇,而且成功刪除”math“這個元素,這是爲什麼呢?上面一個示例明明說了會發生併發修改異常。下面我們還是分析一下其中的特殊原因: 

第一次調用hasNext(): cursor = 0    size = 3, hasNext()返回true
第一次調用iterator.next():
    第一次調用checkForComodification():modCount = 3  expectedModCount = 3
由於 modCount = expectedModCount ,不會發生併發修改異常。
第一次next()方法調用完,cursor遊標的值會加一,所以cursor = 1. 

===============================================================================================================
第二次調用hasNext(): cursor = 1  size = 3, hasNext()返回true
第二次調用iterator.next():
    第二次調用checkForComodification():modCount = 3  expectedModCount = 3
由於 modCount = expectedModCount ,不會發生併發修改異常。
第二次next()方法調用完,cursor遊標的值會加一,所以cursor = 2. 
由於上面的示例中,"math"元素剛好在第二個,所以這時候"math".equals(nextElement)會返回true,
所以會執行集合的刪除元素方法,size會減一,實際修改次數modCount會加一,所以size = 2  modCount = 4

ps:這裏注意cursor遊標的值也是2,size的值也是2,

===============================================================================================================
第三次調用hasNext(): cursor = 2  size = 2, 兩者相等,所以hasNext()返回false,while循環結束,意味着不會調用next()方法,
不會執行調用checkForComodification()方法,那麼肯定就不會發生併發修改異常。

小結論:

  • 當要刪除的元素在集合中的倒數第二個元素的時候,刪除元素不會產生併發修改異常。
  • 原因:因爲在調用hasNext()方法的時候,cursor = size是相等的,hasNext()方法會返回false, 所以不會執行next()方法,也就不會調用checkForComodification()方法,就不會發生併發修改異常。 

四、如何避免併發修改異常?

如何避免併發修改異常還有它的特殊情況呢,其實Iterator迭代器裏面已經提供了remove(),用於在迭代過程對集合結構進行修改,使用iterator.remove()不會產生併發修改異常,爲什麼迭代器的刪除方法不會產生異常呢,我們得去看看Itr內部類的remove()源碼:

//迭代器自帶的刪除方法
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    //校驗是否產生併發修改異常    
    checkForComodification();

    try {
          //真正刪除元素的方法還是調用的ArrayList的刪除方法
         //根據索引進行刪除
         ArrayList.this.remove(lastRet);
        
        cursor = lastRet;
        lastRet = -1;
        //每次刪除完成後,會重新將expectedModCount重新賦值,值就是實際修改次數modCount的值
        //這就保證了,實際修改次數modCount一定會等於預期修改次數expectedModCount ,所以不會產生併發修改異常.
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

小結論:

  • 迭代器調用remove()方法刪除元素,底層還是調用的集合的刪除元素的方法;
  • 在調用remove()方法後,都會將modCount的值賦值給expectedModCount,保證了它兩的值永遠都是相等的,所以也就不會產生併發修改異常; 

五、總結

以上通過幾個示例講解了併發修改異常的現象,以及分析了併發修改異常是如何產生的,在實際工作中,如果需要使用到刪除集合中元素,那麼我們不要使用集合自帶的刪除方法,我們應該使用iterator迭代器給我們提供的刪除方法,這樣可以很大程序避免程序發生併發修改異常ConcurrentModificationException。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章