java.util.ConcurrentModificationException 異常問題(一)

1.1 問題復現 

public void test1()  {
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            arrayList.add(Integer.valueOf(i));
        }

        // 復現方法一
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Integer integer = iterator.next();
            if (integer.intValue() == 5) {
                arrayList.remove(integer);
            }
        }

        // 復現方法二
        iterator = arrayList.iterator();
        for (Integer value : arrayList) {
            Integer integer = iterator.next();
            if (integer.intValue() == 5) {
                arrayList.remove(integer);
            }
        }
    }

 1.2、問題原因分析

先來看實現方法一,方法一中使用Iterator遍歷ArrayList, 拋出異常的是iterator.next(),看下Iterator next方法實現源碼

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

在next方法中首先調用了checkForComodification方法,該方法會判斷modCount是否等於expectedModCount,不等於就會拋出java.util.ConcurrentModificationExcepiton異常。

我們接下來跟蹤看一下modCount和expectedModCount的賦值和修改。

modCount是ArrayList的一個屬性,繼承自抽象類AbstractList,用於表示ArrayList對象被修改次數。

protected transient int modCount = 0;

 

整個ArrayList中修改modCount的方法比較多,有add、remove、clear、ensureCapacityInternal等,凡是設計到ArrayList對象修改的都會自增modCount屬性。

在創建Iterator的時候會將modCount賦值給expectedModCount,在遍歷ArrayList過程中,沒有其他地方可以設置expectedModCount了,因此遍歷過程中expectedModCount會一直保持初始值20(調用add方法添加了20個元素,修改了20次)。

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

在執行next方法時,遇到modCount != expectedModCount方法,導致拋出異常java.util.ConcurrentModificationException。

明白了拋出異常的過程,但是爲什麼要這麼做呢?很明顯這麼做是爲了阻止程序員在不允許修改的時候修改對象,起到保護作用,避免出現未知異常。

 引用網上的一段解釋,點擊查看解釋來源

Iterator 是工作在一個獨立的線程中,並且擁有一個 mutex 鎖。 
Iterator 被創建之後會建立一個指向原來對象的單鏈索引表,當原來的對象數量發生變化時,這個索引表的內容不會同步改變。
當索引指針往後移動的時候就找不到要迭代的對象,所以按照 fail-fast 原則 Iterator 會馬上拋出 java.util.ConcurrentModificationException 異常。
所以 Iterator 在工作的時候是不允許被迭代的對象被改變的。但你可以使用 Iterator 本身的方法 remove() 來刪除對象, Iterator.remove() 方法會在刪除當前迭代對象的同時維護索引的一致性。

再來分析下第二種for循環拋異常的原因:

 public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            action.accept(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

在for循環中一開始也是對expectedModCount採用modCount進行賦值。在進行for循環時每次都會有判定條件modCount == expectedModCount,當執行完arrayList.remove(integer)之後,該判定條件返回false退出循環,然後執行if語句,結果同樣拋出java.util.ConcurrentModificationException異常。

這兩種復現方法實際上都是同一個原因導致的。

 

1.3 問題解決方案

上述的兩種復現方法都是在單線程運行的,先來說明單線程中的解決方案:

public void test2() {
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            arrayList.add(Integer.valueOf(i));
        }

        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Integer integer = iterator.next();
            if (integer.intValue() == 5) {
                iterator.remove();
            }
        }
    }

 這種解決方案最核心的就是調用iterator.remove()方法。我們看看該方法源碼爲什麼這個方法能避免拋出異常

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

 

在iterator.remove()方法中,同樣調用了ArrayList自身的remove方法,但是調用完之後並非就return了,而是expectedModCount = modCount重置了expectedModCount值,使二者的值繼續保持相等。

針對forEach循環並沒有修復方案,因此在遍歷過程中同時需要修改ArrayList對象,則需要採用iterator遍歷。

上面提出的解決方案調用的是iterator.remove()方法,如果不僅僅是想調用remove方法移除元素,還想增加元素,或者替換元素,是否可以呢?瀏覽Iterator源碼可以發現這是不行的,Iterator只提供了remove方法。

但是ArrayList的內部類ListItr實現了ListIterator接口,並繼承了內部類Iter(實現了Iterator<E>),這些操作都是可以實現的,使用示例如下:

public void test3() {
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            arrayList.add(Integer.valueOf(i));
        }

        ListIterator<Integer> iterator = arrayList.listIterator();
        while (iterator.hasNext()) {
            Integer integer = iterator.next();
            if (integer.intValue() == 5) {
                iterator.set(Integer.valueOf(6));
                iterator.remove();
                iterator.add(integer);
            }
        }
    }

 二、 多線程情況下的問題分析及解決方案

2.1 問題復現

public void test4() {
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            arrayList.add(Integer.valueOf(i));
        }

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                ListIterator<Integer> iterator = arrayList.listIterator();
                while (iterator.hasNext()) {
                    System.out.println("thread1 " + iterator.next().intValue());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                ListIterator<Integer> iterator = arrayList.listIterator();
                while (iterator.hasNext()) {
                    System.out.println("thread2 " + iterator.next().intValue());
                    iterator.remove();
                }
            }
        });
        thread1.start();
        thread2.start();
    }

在個測試代碼中,開啓兩個線程,一個線程遍歷,另外一個線程遍歷加修改。程序輸出結果如下 

thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
thread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
thread2 14
thread2 15
thread2 16
thread2 17
thread2 18
thread2 19
Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at com.snow.ExceptionTest$1.run(ExceptionTest.java:74)
    at java.lang.Thread.run(Thread.java:745)

Process finished with exit code 0

 2.2 問題分析

從上面代碼執行結果可以看出thread2 遍歷結束後,thread1 sleep完1000ms準備遍歷第二個元素,next的時候拋出異常了。我們從時間點分析一下拋異常的原因

時間點 arrayList.modCount thread1 iterator.expectedModCount thread2 iterator.expectedModCount
thread start,初始化iterator 20 20 20
thread2.remove()調用之後 21 20 21

 

 

 

 

 兩個thread都是使用的同一個arrayList,thread2修改完後modCount = 21,此時thread2的expectedModCount = 21 可以一直遍歷到結束;thread1的expectedModCount仍然爲20,因爲thread1的expectedModCount只是在初始化的時候賦值,其後並未被修改過。因此當arrayList的modCount被thread2修改爲21之後,thread1想繼續遍歷必定會拋出異常了。

 

在這個示例代碼裏面,兩個thread,每個thread都有自己的iterator,當thread2通過iterator方法修改expectedModCount必定不會被thread1感知到。這個跟ArrayList非線程安全是無關的,即使這裏面的ArrayList換成Vector也是一樣的結果,不信上測試代碼:

public void test5() {
        Vector<Integer> vector = new Vector<>();
        for (int i = 0; i < 20; i++) {
            vector.add(Integer.valueOf(i));
        }

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                ListIterator<Integer> iterator = vector.listIterator();
                while (iterator.hasNext()) {
                    System.out.println("thread1 " + iterator.next().intValue());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                ListIterator<Integer> iterator = vector.listIterator();
                while (iterator.hasNext()) {
                    Integer integer = iterator.next();
                    System.out.println("thread2 " + integer.intValue());
                    if (integer.intValue() == 5) {
                        iterator.remove();
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }

 

 執行後輸出結果爲:

thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
thread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
thread2 14
thread2 15
thread2 16
thread2 17
thread2 18
thread2 19
Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.Vector$Itr.checkForComodification(Vector.java:1184)
    at java.util.Vector$Itr.next(Vector.java:1137)
    at com.snow.ExceptionTest$3.run(ExceptionTest.java:112)
    at java.lang.Thread.run(Thread.java:745)

Process finished with exit code 0

 

test5()方法執行結果和test4()是相同的,那如何解決這個問題呢? 

 2.3 多線程下的解決方案

2.3.1 方案一:iterator遍歷過程加同步鎖,鎖住整個arrayList

public static void test5() {
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            arrayList.add(Integer.valueOf(i));
        }

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (arrayList) {
                    ListIterator<Integer> iterator = arrayList.listIterator();
                    while (iterator.hasNext()) {
                        System.out.println("thread1 " + iterator.next().intValue());
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (arrayList) {
                    ListIterator<Integer> iterator = arrayList.listIterator();
                    while (iterator.hasNext()) {
                        Integer integer = iterator.next();
                        System.out.println("thread2 " + integer.intValue());
                        if (integer.intValue() == 5) {
                            iterator.remove();
                        }
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }

下接:https://blog.csdn.net/wsen1229/article/details/103291059 

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