[直擊native] 單步調試ArrayList 源碼 二

[直擊native] 單步調試ArrayList 源碼 二

上一節講到iterator() , iterator() 方法涉及到ArrayList的內部類Itr , 這一節重點講解 這個迭代器 ltr

將之前先放一段問題代碼

 @Test
    public  void preLoopTest(){

        List<String> arr = new ArrayList<>();
        arr.add("1");
        arr.add("4");
        Iterator<String> iterator = arr.iterator();
        while (iterator.hasNext()) {
            String next = iterator.next();
            if ("4".equals(next)) {
                iterator.remove();
            }
        }
        System.out.println(Arrays.toString(arr.toArray()));

        List<String> arr1 = new ArrayList<>();
        arr1.add("1");
        arr1.add("4");
        for (String a : arr1) {//java.util.ConcurrentModificationException
            if ("4".equals(a)) {
                arr1.remove(a);
            }
        }
        System.out.println(Arrays.toString(arr1.toArray()));

    }

如果反編譯過源碼的同學應該知道

for (String a : arr1)

反編譯過來for 循環 也是 使用了迭代器 , 那麼問題出在哪裏?我們從源碼中找問題所在 .

找到異常(java.util.ConcurrentModificationException)代碼


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

	final void checkForComodification() {
            if (modCount != expectedModCount)//判斷modCount 修改次數不等於 expectedModCount
                throw new ConcurrentModificationException();
    }

那麼這個expectedModCount 是代表什麼?

看內部迭代器源碼

快速失敗(fail—fast)

在用迭代器遍歷一個集合對象時,如果遍歷過程中對集合對象的內容進行了修改(增加、刪除、修改),則會拋出Concurrent Modification Exception。

​ 原理:迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變modCount的值。每當迭代器使用hashNext()/next()遍歷下一個元素之前,都會檢測modCount變量是否爲expectedmodCount值,是的話就返回遍歷;否則拋出異常,終止遍歷。

​ 注意:這裏異常的拋出條件是檢測到 modCount!=expectedmodCount 這個條件。如果集合發生變化時修改modCount值剛好又設置爲了expectedmodCount值,則異常不會拋出。因此,不能依賴於這個異常是否拋出而進行併發操作的編程,這個異常只建議用於檢測併發修改的bug。

​ 場景:java.util包下的集合類都是快速失敗的,不能在多線程下發生併發修改(迭代過程中被修改)。

所以在 new ltr 對象時傳出對象包含了修改次數,這是爲併發做的檢測操作 , 但是在循環遍歷過程中修改元素也可以看做另類的併發.

此時我們已經知道了原因,但是爲什麼直接使用迭代器刪除不會出現異常?

下面繼續分析一下源碼流程

	 /**
	 	按正確的順序返回列表中元素的迭代器。
     * Returns an iterator over the elements in this list in proper sequence.
     *	這個返回的迭代器是快速失敗(fail—fast) 
     * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
     *
     * @return an iterator over the elements in this list in proper sequence
     */
    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 的remove 方法 將之前,調用next返回的對象刪除
                ArrayList.this.remove(lastRet);
                // 可以回憶下remove 方法中有數組拷貝的函數將刪除元素右邊的全部左移一位,所以這裏將指針指向左移一位
                cursor = lastRet;
                // 再將上一個置爲-1 
                lastRet = -1;
                // 關鍵操作: 刪除之後再此對modCount同步到了預期修改值expectedModCount
                // 所以再次調用next 值時 不會出現異常 , 並且使用這個函數 同期修改了cursor
                // 所以在hasNext 便會體現真正大小 
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        // 這個函數是在1.8加入的新方法 , 用於增強遍歷 , 可以結合lambda 表達式遍歷數據
        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            // 傳入參數 接口實現類 不爲空
            Objects.requireNonNull(consumer);
            // 數組大小
            final int size = ArrayList.this.size;
            // 當前遊標
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            // 邊界判斷
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            // 對數組每個對象執行傳入的固定的接口方法 
            // 遍歷爲當前遊標到末尾 並且是修改次數沒變化的前提下
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // 更新最後的遊標 和最後一次之前的遊標位置
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            // 最後再此檢測是否修改 
            // 所以如果在傳入參數中的方法中修改也會出現併發修改異常
            checkForComodification();
        }

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


最後總結

如果是需要遍歷時候修改集合內元素,可以使用set 方法

如果需要刪除集合內的元素可以使用迭代器的remove 方法

   if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}



### 最後總結

如果是需要遍歷時候修改集合內元素,可以使用set 方法

如果需要刪除集合內的元素可以使用迭代器的remove 方法

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