迭代器Iterator與ConcurrentModificationException詳解

背景:一直以來對迭代器的問題理解不是很透徹,特別是迭代器和異常ConcurrentModificationException之間的聯繫。經過debug,詳細瞭解其底層的具體實現過程。java

簡介

 

Iterator必須依附於Collection對象,如有一個Iterator對象,則必然有一個與之關聯的Collection對象。面試

Iterator提供了兩個方法來迭代訪問Collection集合裏的元素,並可經過remove()來刪除集合中上一次next()方法返回的集合元素。編程

當使用Iterator迭代訪問Collection集合元素時,Collection集合裏的元素不能被改變,只有經過Iterator的remove()方法刪除上一次next()方法返回的集合元素才能夠;不然會引起java.util.ConcurrentModificationException異常。數組

之因此會出現這樣的異常,是由於Iterator迭代器採用的是快速失敗(fast-fail)機制,一旦在迭代過程當中檢測到該集合已經被修改(一般是程序中其它線程修改),程序當即引起ConcurrentModificationException,安全

而不是顯示修改後結果,這樣能夠避免共享資源而引起的潛在問題。數據結構

 ConcurrentModificationException發生在Iterator#next()方法實現中,每次調用都會檢查容器的結構是否發生變化,目的是爲了不共享資源而引起的潛在問題。多線程

觀察HashMap和ArrayList底層Iterator#next(), 能夠看到fast-fail只會增長或者刪除(非Iterator#remove())拋出異常;改變容器中元素的內容不存在這個問題(主要是modCount沒發生變化)。app

在單線程中使用迭代器,對非線程安全的容器,可是隻能用Iterator#remove;不然會拋出異常。ide

在多線程中使用迭代器,可使用線程安全的容器來避免異常。工具

使用普通的for循環遍歷,效率雖然比較低下,可是不存在ConcurrentModificationException異常問題。用的也比較少。

ps:java在設計工具類時候,分別設計出線程安全和非安全的工具類,也是致力於解決這些多線程操做問題。因此無須糾結,直接使用就行。

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

在觀察底層實現時,能夠看到容器對象的modCount值在改變容器結構時才發生改變。

集合Collection Map改變 對迭代器遍歷的影響

這裏說的容器結構的改變是指 增長 或者刪除元素,致使集合的大小發生改變。

觀察源碼發現,不論Collection或Map,對於Iterator來講:

異常是在next方法中拋出的,咱們在使用迭代器的時候,通常會先進行hasNext方法的判斷,再調用next方法來使用元素。

如下是對於ArrayList、 HashMap、ConcurrentHashMap三個容器的迭代器測試用例:

/**
 * Project Name:Spring0725
 * File Name:Test5.java
 * Package Name:work1201.basic
 * Date:2017年12月1日下午4:16:25
 * Copyright (c) 2017, 深圳金融電子結算中心 All Rights Reserved.
 *
*/

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.junit.Test;

/**
 * ClassName:TestIterator <br/>
 * Function: 測試Collection Map 改變集合結構對迭代器遍歷有無影響
 * 在使用Iterator時候,對於Collection集合,改變集合的結構會觸發ConcurrentModificationException異常;改變集合中元素的內容不會觸發異常
 * 對於Map集合,線程安全map——ConcurrentHashMap改變集合Map的結構,無異常發生
 * 對於非線程安全的HashMap,使用Iterator遍歷的時候,改變集合的結構,會觸發ConcurrentModificationException
 * 
 * 非線程安全類的Collection或者Map,在使用Iterator過程當中,有時候改變容器結構,並不會發生異常,這主要和底層的實現有關。
 * 如:List刪除倒數第二個元素無異常;Map刪除set集合中的最後一個元素,無異常
 * Date:     2017年12月1日 下午4:16:25 <br/>
 * @author   prd-lxw
 * @version   1.0
 * @since    JDK 1.7
 * @see      
 */
public class TestIterator {

    /**
     * 方法1
     * 迭代器更改Collection集合結構,非倒數第二個元素,會發生異常
     */
    @Test(expected = ConcurrentModificationException.class)
    public void testDeletCollection() {
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < 20; i++) {
            list.add(i + "");
        }

        Iterator<String> it = list.iterator();
        String ss = 19 + "";
        while (it.hasNext()) {
            if (it.next().equals(ss)) {
                System.out.println("找到元素:" + ss);
                list.remove(ss); //集合大小發生改變 ConcurrentModificationException
                //                list.add(102+""); //集合大小發生改變 ConcurrentModificationException
                //                list.set(19, 211+"");  //不會觸發異常,由於沒有改變Collection集合的大小
                //                it.remove();//正常
            }
        }
    }

    /**
     * 方法2
     * 刪除Collection集合的倒數第二個元素,不會發生異常
     */
    @Test
    public void testDeletBack2Collection() {
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < 20; i++) {
            list.add(i + "");
        }

        Iterator<String> it = list.iterator();
        String ss = 18 + "";//倒數第2個元素
        while (it.hasNext()) {
            if (it.next().equals(ss)) {
                System.out.println("找到元素:" + ss);
                list.remove(ss); //集合大小發生改變 ConcurrentModificationException
                //              list.add(102+""); //集合大小發生改變 ConcurrentModificationException
                //              it.remove();//正常
            }
        }
    }

    /**
     * 方法3
     * 普通for方法遍歷Collection,改變集合結構無異常
     */
    @Test
    public void testDeletCollectionFor() {
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < 20; i++) {
            list.add(i + "");
        }
        String ss = 15 + "";
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).equals(ss)) {
                list.remove(i);
            }
        }

    }

    /**
     * 方法4
     * 使用加強型的foreach遍歷Collection,改變集合結構,會發生異常
     * 起底層實現和使用Iterator一致
     */
    @Test(expected = ConcurrentModificationException.class)
    public void testDeletCollectionForEach() {
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < 20; i++) {
            list.add(i + "");
        }
        String ss = 18 + "";
        for (String str : list) {
            if (str.equals(ss)) {
                list.remove(str);
            }
        }

    }

    /**
     * 方法5
     * 使用迭代器的過程當中,改變map的結構,會觸發ConcurrentModificationException異常
     * 可是若是刪除的key在entrySet的結尾,好比key=10+""  就不會發生這個異常
     */
    @Test(expected = ConcurrentModificationException.class)
    public void testIteratorMapEntry() {
        HashMap<String, String> map = new HashMap<String, String>();
        for (int i = 0; i < 20; i++) {
            map.put(i + "", i + "");
        }
        Set<Entry<String, String>> entrySet = map.entrySet(); //打印entrySet集合能夠發現,key=10是集合的最後一個元素
        Iterator<Entry<String, String>> it = entrySet.iterator();
        String key = 10 + "";
        while (it.hasNext()) {
            if (it.next().getKey().equals(key)) {
                System.out.println("testIteratorMapEntry找到元素:" + key);
                //改變Map
                //                map.remove(key); //ConcurrentModificationException
                map.put(21 + "", 21 + ""); //ConcurrentModificationException
                //                map.replace(key, 30 + "");//正常
                //                it.remove(); //正常
                System.out.println(map.size() + ":" + map.get(key));
            }
        }
    }

    /**
     * 方法6
     * 使用迭代器的過程當中,改變map的結構,會觸發ConcurrentModificationException異常
     * 可是若是刪除的key在keySet的結尾,好比key=10+""  就不會發生這個異常
     * 
     * 對於非線程安全的map 使用Iterator遍歷 keySet valueSet entrySet實驗結果都一致
     */
    @Test(expected = ConcurrentModificationException.class)
    public void testIteratorMapKey() {
        HashMap<String, String> map = new HashMap<String, String>();
        for (int i = 0; i < 20; i++) {
            map.put(i + "", i + "");
        }

        Set<String> mapKeySet = map.keySet();//打印keySet集合能夠發現,key=10是集合的最後一個元素
        Iterator<String> it = mapKeySet.iterator();
        String key = 11 + "";
        while (it.hasNext()) {
            if (it.next().equals(key)) {
                System.out.println("testIteratorMapKey找到元素:" + key);
                //改變Map
                map.remove(key); //ConcurrentModificationException
                //                map.put(21 + "", 21 + ""); //ConcurrentModificationException
                //                map.replace(key, 30 + "");//正常
                //                it.remove(); //正常
                System.out.println(map.size() + ":" + map.get(key));
            }
        }
    }

    //    ################# 線程安全類ConcurrentHashMap不存在ConcurrentModificationException問題
    /**
     * 方法7
     * 線程安全類Map entrySet操做,無異常發生
     */
    @Test
    public void testConIteratorMapEntry() {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
        for (int i = 0; i < 20; i++) {
            map.put(i + "", i + "");
        }

        Iterator<Entry<String, String>> it = map.entrySet().iterator();
        String key = 12 + "";
        while (it.hasNext()) {
            if (it.next().getKey().equals(key)) {
                System.out.println("testConIteratorMapEntry找到元素:" + key);
                //改變Map
                map.remove(key); //正常
                //                map.put(21 + "", 21 + ""); //正常
                //                map.replace(key, 30 + "");//正常
                //                it.remove(); //正常
                System.out.println(map.size() + ":" + map.get(key));
            }
        }
    }

    /**
     * 方法8
     * 無異常
     */
    @Test
    public void testConIteratorMapKey() {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
        for (int i = 0; i < 20; i++) {
            map.put(i + "", i + "");
        }

        Iterator<String> it = map.keySet().iterator();
        String key = 10 + "";
        while (it.hasNext()) {
            if (it.next().equals(key)) {

                System.out.println("testConIteratorMapKey找到元素:" + key);
                //改變Map
                //                map.remove(key); //正常
                map.put(21 + "", 21 + ""); //正常
                map.replace(key, 30 + "");//正常
                //                it.remove(); //正常
                System.out.println(map.size() + ":" + map.get(key));
            }
        }
    }

}
View Code

 

 

 

 ArryList的迭代器底層實現

問題

觀察上述方法1 和方法2 ,能夠發現一個問題——

在使用迭代器遍歷ArrayList時候,若是刪除的是鏈表中倒數第二個元素,不會發生ConcurrentModificationException異常,不然就會發生異常。

方法1 和2實現同樣,只不過刪除的元素下標不同。

分析

分析ArrayList中Iterator中next() 和hasNext() 的具體實現

從底層實現來理解上述差別產生的緣由:

由於異常都是發生在Iterator#next()方法中,因此能夠打開iterator()方法的實現。

 Iterator<String> it = list.iterator();

將ArrayList.class中關於迭代器的代碼摘錄以下

 /**
     * Returns an iterator over the elements in this list in proper sequence.
     * 經過這個方法獲取到list的迭代器   內部類Itr()實現了Iterator接口
     * <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; //迭代器初始化時候設置的,一旦容器結構發生變化,會改變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();
            }
        }

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

從源碼能夠看到,ArrayList#iterator()調用的是Itr類,Itr實現了Iterator接口,該類中實現了接口中的next()和hasNext()方法,因此

ArrayList#iterator()#hasNext() 就是調用Itr#hasNext()

ArrayList#iterator()#next() 就是調用Itr#next()

緣由 

執行hasNext()會判斷下一個元素下標與集合大小是否相等(元素存在與否),相等返回false;不等 返回true;
ps:對於一個list的,lastIndex = size-1 在執行next()方法前會進行結構是否變化的檢查,modCount != expectedModCount,返回true就拋異常,返回false就不拋出異常。 ps:一旦容器結構發生變化,modCount的值會發生變化,每次累加1;expectedModCount在生成迭代器時候進行初始化,表明初始化時候容器的modCount。.modCount是屬於ArrayList對象的,expectedModCount是屬於迭代器對象的。 每次next()方法執行完以後,cursor都表明當前元素的下一個元素的下標。

而對於方法1和方法2:

hasNext()的判斷是根據下一個元素的下標是否與容器的大小相等作判斷
刪除倒數第二個元素沒有異常; cursor == size
刪除非倒數第二個元素有異常:cursor != size,因此會進入next(),而容器結構的變化致使modCount != expectedModCount,從而拋出異常。

Map與迭代器之間的底層實現

問題

這裏討論的是非線程安全類的Map,對Map的keySet、 valueSet、 entrySet三個集合可使用Iterator進行遍歷。

這裏舉例解釋方法6中存在的問題:

map中放入0-19個元素,刪除key=10+「」會拋出異常;其它的則不會。

分析

分析HashMap中Iterator中next() 和hasNext() 的具體實現

打開方法6中的 HashMap#keySet()實現

 Set<String> mapKeSet = map.keySet();//打印keySet集合能夠發現,key=10是集合的最後一個元素
        Iterator<String> it = mapKeSet.iterator();

 

 在HashMap.class中能夠看到以下的實現

  public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

    final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); } //Iterator接口的具體實現類 public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

 

分析: HashMap#keySet()覆寫了父類AbstractMap#keySet()方法,跟蹤變量keySet的初始化過程,能夠發現其初始值默認爲null。

 HashMap#keySet()方法中 接口Set的實現類爲KeySet,進一步跟蹤能夠發現,mapKeSet.iterator()返回的對象是實例化KeyIterator生成的對象,跟蹤改類的具體實現——

下面是Map與Iterator的核心代碼

    // iterators

    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() { return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

    final class ValueIterator extends HashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

 

咱們從源碼能夠看到 KeyIterator實現了Iterator接口,而且繼承了抽象類HashIterator,在該類中實現了接口Iterator#next()方法;而KeyIterator#next()又是調用其父類HashIterator#nextNode()方法。

因此其底層實現:

HashMap#keySet()#iterator()#hasNext()  調用的是HashIterator#hasNext()
HashMap#keySet()#iterator()#next()  調用的就是HashIterator#nextNode().key

緣由

對於Map:
進行hasNext 是判斷next是否爲null,true-後面有元素。false-後面沒元素。
執行next的時候,會進行map結構的檢查,modCount != expectedModCount,返回true就拋異常,返回false就不拋出異常。
 
當咱們刪除keySet最後一個元素時候,hasNext返回false,不會進入Next,天然不發生異常
當刪除非最後一個元素的時候,執行next的時候觸發結構檢查,發生異常。
是不是最後一個能夠經過觀察set的輸出結果。

在方法6中,經過debug能夠觀察到mapKeySet中的key值排列以下

[11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

 

當刪除key=10+「」元素的時候,迭代器循環結束,不會在進入hasNext方法,因此就沒有異常產生了。

多線程環境下,程序示例

多線程環境下的結果和單測中的結果一致,這裏只是爲了模擬觀察

/**
 * Project Name:Spring0725
 * File Name:IteratorListDelete.java
 * Package Name:work12.day05
 * Date:2017年12月5日下午9:58:53
 * Copyright (c) 2017, 深圳金融電子結算中心 All Rights Reserved.
 *
 */

package work12.day05;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * ClassName:IteratorListDelete <br/>
 * Function: 測試多線程下 改變Collection集合 對迭代器遍歷形成的影響 Date: 2017年12月5日 下午9:58:53 <br/>
 * 
 * @author prd-lxw
 * @version 1.0
 * @since JDK 1.7
 * @see
 */
public class IteratorListDelete {
    private final ArrayList<String> list;

    public IteratorListDelete(ArrayList<String> list) {
        super();
        this.list = list;
    }

    public ArrayList<String> getList() {
        return list;
    }

    public static void main(String[] args) {
        ArrayList<String> aList = new ArrayList<String>();
        IteratorListDelete tt = new IteratorListDelete(aList);
        for (int i = 0; i < 100; i++) {
            tt.getList().add(i + "");
        }
        new Thread(new TraverseList(tt), "子線程").start();
        // System.out.println(tt.getList().get(28));
        try {
            Thread.sleep(10);
            // tt.getList().remove(28+""); // 集合大小發生改變 ConcurrentModificationException
            tt.getList().add(101 + ""); // 集合大小發生改變 ConcurrentModificationException
            // tt.getList().set(28, 201+""); //改變集合內容,不會觸發異常,由於沒有改變Collection集合的大小
        } catch (InterruptedException e) {
            e.printStackTrace();

        }

    }

}

/**
 * ClassName: TraverseList <br/>
 * Function: 線程一直循環遍歷collection集合 date: 2017年12月5日 下午10:35:06 <br/>
 *
 * @author prd-lxw
 * @version 1.0
 * @since JDK 1.7
 */
class TraverseList implements Runnable {
    private final IteratorListDelete tt;

    public TraverseList(IteratorListDelete tt) {
        super();
        this.tt = tt;
    }

    public void run() {
        try {
            Thread.sleep(5);
        } catch (Exception e) {
            // TODO: handle exception
        }
        while (true) {
            Iterator<String> it = tt.getList().iterator();
            while (it.hasNext()) {
                System.out.println(Thread.currentThread().getName() + "循環遍歷:"
                        + it.next());
            }
        }

    }

}
View Code

java中爲何要使用迭代器

ps:阿里面試時候問到這個問題,當時是一臉的懵逼。

迭代模式是訪問集合類的通用方法,只要集合類實現了Iterator接口,就能夠用迭代的方式來訪問集合類內部的數據,Iterator訪問方式把對不一樣集合類的訪問邏輯抽象出來,使得不用暴露集合內部的結構而達到循環遍歷集合的效果。 
例如,若是沒有使用Iterator,遍歷一個數組的方法是使用索引:

Ruby代碼 
for(int i=0; i<array.length; i++) { ... get(i) ... }  


   這種方法的缺點就是事先必須知道集合的數據結構,並且當我換了一種集合的話代碼不可重用,要修改,好比我用set,就不能經過索引來遍歷了。訪問代碼和集合是緊耦合,沒法將訪問邏輯從集合類和客戶端代碼中剝離出來,每一種集合類對應一種訪問方式,代碼不可重用。 
   爲解決以上問題,Iterator模式老是用同一種邏輯來遍歷集合。 
   每一種集合類返回的Iterator具體類型可能不一樣,Array可能返回ArrayIterator,Set可能返回SetIterator,Tree 可能返回TreeIterator,可是它們都實現了Iterator接口,所以,客戶端不關心究竟是哪一種Iterator,它只須要得到這個 Iterator接口便可,這就是面向對象的威力。 
這就是針對抽象編程的原則:對具體類的依賴性最小

相關文章
相關標籤/搜索