1. 程式人生 > >Java併發修改錯誤ConcurrentModificationException分析

Java併發修改錯誤ConcurrentModificationException分析

1. 介紹
併發修改ConcurrentModificationException錯誤是開發中一個常見錯誤,多發生在對一個Collection邊遍歷邊做影響size變化的操作中,下面以ArrayList為例分析ConcurrentModificationException錯誤。

2. 分析
ArrayList初始資料如下。

List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);

場景1:不會有併發修改錯誤

int length = list.size();
        for (int i = 0; i < length; i++) {
            if (list.get(i).equals(2)) {
                list.add(10);
            }
        }

場景2:會有併發修改錯誤

for(int temp : list) {
            if(temp == 2) {
                list.add(10);
            }
        }

場景3:會有併發修改錯誤

Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()) {
            if(iterator.next().equals(2)) {
                list.add(10);
            }
        }

場景4:沒有併發修改問題

ListIterator<Integer> listIterator = list.listIterator();
        while (listIterator.hasNext
()) { if (listIterator.next().equals(2)) { listIterator.add(10); } }

其實ConcurrentModificationException異常的丟擲是由於checkForComodification(AbstractList類中)方法的呼叫引起的

private void checkForComodification() {
        if (this.modCount != l.modCount)
            throw new ConcurrentModificationException();
    }

而checkForComodification方法的呼叫發生在Iterator相關api方法中,
在呼叫list的iterator方法會建立一個Itr物件、
這裡寫圖片描述
在建立會與AbstractList的modCount賦予相同的值, 而在Itr的next方法中會呼叫checkForComodification
這裡寫圖片描述

在場景3中,list.add操作更改modCount的值,所以會有併發修改錯誤
而場景1中並沒有使用iterator相關api,add操作雖然修改了modCount但是不會檢查modCount所以沒有併發修改錯誤。
場景4中,ListItr類add方法

 public void add(E e) {
            checkForComodification();

            try {
                int i = cursor;
                ArrayList.this.add(i, e);
                cursor = i + 1;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

其中9行:在呼叫了list.add操作之後,將ListItr中的expectedModCount與AbstractList中的modCount進行了同步,所以在下次呼叫next也就不會丟擲異常了,此時假如以後不呼叫next或者又重新建立了 ListItr也不會有異常丟擲。
最後場景2並沒有使用Iterator中的api為什麼也丟擲了異常了。其實編譯器會將for-each迴圈程式碼編譯為Iterator相關api的呼叫。
為了便於檢視編譯後的程式碼這裡新增一個“———-”列印。

List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println("------------");
        for (int temp : list) {
            if (temp == 2) {
                list.add(10);
            }
        }

編譯後的位元組碼為:
這裡寫圖片描述
所以場景2和場景3是一樣的,也會丟擲異常了。