集合List之ConcurrentModificationException異常分析


一、前言  

  Java中,集合類ArrayList不管是在開發工作中,還是在面試中,都應該是個比較高頻出現的知識點。在使用過程中,可能會遇到迭代刪除的需求場景,此時如果代碼書寫不當,極有可能會拋出 java.util.ConcurrentModificationException 異常信息。下面對這個異常做點分析,為什么會出現異常,怎樣去正確的迭代刪除。

二、異常原因分析

  測試代碼如下:

package com.cfang.prebo.oTest;

import java.util.Iterator;
import java.util.List;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;

public class TestListException {

	public static void main(String[] args) {
		List<Integer> list = Lists.newArrayList();
		list.add(1);
		Iterator<Integer> iterator = list.iterator();
		while(iterator.hasNext()) {
			Integer val = iterator.next();
			if(val == 1) {
				list.remove(val);
//				iterator.remove();
			}
		}
		System.out.println("result:" + JSON.toJSONString(list));
	}
}

  運行結果:

  從異常棧信息中可以看出,最終拋出此異常的方法,是 checkForComodification 方法。下面進行追根逐源的看看,為什么方法會拋出異常。

  首先整體貼出迭代器 Iterator 對 List 進行迭代的關鍵性代碼片段:

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

    /**
     * An optimized version of AbstractList.Itr
     */
    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;

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

        @SuppressWarnings("unchecked")
        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();
            }
        }

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

  對List的迭代iterator會new出個Itr對象的引用,Itr是個成員內部類實現。其中幾個關鍵性的屬性:

    cursor - 游標索引,表示下一個可訪問的元素的索引

    lastRet - 還是索引,是上一個元素的索引。默認值-1

    expectedModCount - 對集合的修改期望值,初始值等於modCount

    modCount的定義在AbstractList中,初始值為0,如下定義:

protected transient int modCount = 0;

    該值會在List的方法add以及remove中,進行加1操作,如下代碼片段:

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;
    }

  

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

  也就是說,在進行add和remove的時候,都會將modCount值修改。

  好了,鋪墊到這里,就可以來結合測試main方法來進行一步步的解釋說明了:

    1、初始化ArrayList,調用list.add方法,此時,modCount=1,list.size = 1

    2、初始化itreator迭代循環。此時,expectedModCount = modCount = 1,cursor默認值0,lastRet默認值-1

    3、itreator.hasNext方法判斷,cursor != size成立,有元素可訪問,進入循環

    4、調用itreator.next方法,校驗后獲取值。此時expectedModCount == modCount成立,校驗通過。獲取值並設置相關屬性 lastRet = 0,cursor = 1

    5、調用list.remove方法,modCount加1。此時,modCount=2,list.size = 0

    6、itreator.hasNext方法判斷,cursor != size成立,進入循環

    7、調用itreator.next方法,校驗方法checkForComodification,此時,expectedModCount != modCount成立,拋出ConcurrentModificationException異常

  寫到這里,基本上為啥會出現異常,應該是已經非常明了清晰了。總結起來就是:如果是使用list.remove的話,會導致expectedModCount != modCount條件成立,也即兩個的值會不等。當然了,使用for-each迭代也是一樣的,畢竟for-each底層如果是對集合遍歷的話,也還是利用itreator去做的。

  說完原因呢,下面簡單說說解決辦法:

  單線程情況下:可以使用迭代器itreator提供的remove,從源碼中可以看出,在方法中會對cursor、lastRet重設值,將expectedModCount重新設值為modCount。

  多線程情況下:1、迭代刪除使用鎖 - synchronized或者lock

         2、創建安全的容器 - Collections.synchronizedList方法、CopyOnWriteArrayList


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2024 CODEPRJ.COM