Java 迭代器引起 ConcurrentModificationException

引言

ConcurrentModificationException這個異常你們都很熟悉,當在forEach進行刪除時都會出現該異常。java

若是你還不瞭解,請參考澍澍的博客:關於在list循環的過程當中進行刪除的處理 - 晨澍的博客git

clipboard.png

ConcurrentModificationException的解決方案之一是使用迭代器,可是不表明迭代器就一勞永逸了。github

使用的時候還需斟酌數組的索引。算法

描述

問題場景

以下圖所示:編程

clipboard.png

原來的同步方法獲取的節點是節點的父節點,根據父節點進行對應。數組

然而在同步更新文件的時候,發現這樣並很差處理,在不改動原代碼的狀況下,設計了將列表轉爲樹型結構的方法,這樣能夠從根節點向下開始遍歷,便於操做。安全

也是在牛客網身經百戰,實現這個難度不大。但在編寫相關實現的時候,遇到了一個小問題。多線程

迭代器智能嗎?

第一步,將列表中的根節點找出來。併發

@Override
public ClusterNode listToTree(List<ClusterNode> clusterNodes) {
    logger.debug("聲明根節點名稱");
    final String ROOT_NAME = "ROOT";

    logger.debug("聲明根節點");
    ClusterNode rootNode = null;

    logger.debug("獲取迭代器,遍歷節點列表");
    Iterator<ClusterNode> iterator = clusterNodes.iterator();
    while (iterator.hasNext()) {

        logger.debug("向後遍歷");
        ClusterNode clusterNode = iterator.next();

        if (ROOT_NAME.equals(clusterNode.getName())) {
            logger.debug("獲取到根節點,賦值,並從原列表中移除");
            rootNode = clusterNode;
            iterator.remove();
            break;
        }
    }

    logger.debug("設置子節點");
    assert rootNode != null;
    setChildrenNode(rootNode, clusterNodes);

    return rootNode;
}

第二步,再從根節點開始,遞歸設置子節點。ide

/**
 * 爲節點設置符合條件的子節點,同時遞歸,設置子節點的子節點
 * @param parentNode   父節點
 * @param clusterNodes 子節點列表
 */
private void setChildrenNode(ClusterNode parentNode, List<ClusterNode> clusterNodes) {
    logger.debug("清空原集合");
    parentNode.getClusterNodes().clear();

    logger.debug("遍歷列表");
    Iterator<ClusterNode> iterator = clusterNodes.iterator();
    while (iterator.hasNext()) {
        ClusterNode clusterNode = iterator.next();

        logger.debug("若是父節點匹配");
        if (clusterNode.getParentClusterNode().getName().equals(parentNode.getName())) {
            logger.debug("將當前節點添加到父節點的子列表中");
            parentNode.getClusterNodes().add(clusterNode);

            logger.debug("移除該節點");
            iterator.remove();

            logger.debug("遞歸設置子節點");
            setChildrenNode(clusterNode, clusterNodes);
        }
    }
}

思想確定是沒問題的。

調用了一行遞歸,當我在編寫這行代碼的時候就已經察覺到可能不對了,由於本次迭代器尚未迭代完,條件符合時就去遞歸,而後遞歸又去新建迭代器,遞歸中又可能刪除新元素,再遞歸回來,繼續迭代的結果還正確嗎?

logger.debug("遞歸設置子節點");
setChildrenNode(clusterNode, clusterNodes);

本着徹底相信JDK的思想,興許我憂慮的事情,其實大牛們早就幫我解決了呢?雖然感受可能不對,但仍是這樣寫了。想着把測試用例寫得完善一些,錯了再改唄!

測試

一測試,果真出錯了!在調用next方法時拋出了ConcurrentModificationException異常,看來,迭代器尚未智能到如此地步。

clipboard.png

源碼

翻開ArrayList中迭代器的源碼。(自從上次在慕課網的驅動下,強制本身閱讀了HashMap的源碼後,發現本身對讀源碼沒那麼抗拒了。)

clipboard.png

在刷過編程題後,終於明白爲何這些家公司都問HashMap源碼,HashMap真的是過重要了,能夠在實際業務中大大下降算法的時間複雜度!

/**
 * Returns an iterator over the elements in this list in proper sequence.
 *
 * <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();
}

迭代器方法內部就返回了一個Itr的對象,這是ArrayList中的私有內部類,爲何要用內部類呢?一大好處就是內部類能夠直接訪問外部類的私有成員,具體進行集合操做的時候很是方便。

ConcurrentModificationException,由於十分常見,因此咱們經常只關注了這個異常怎麼出現的,每每忽略了異常自己。

併發修改異常,最簡單的場景,就是線程不安全的多線程併發場景。

在迭代器對象執行操做以前,都會執行checkForComodification方法,以判斷當前操做下是否安全。就像下面的代碼實現同樣,判斷當前的數量是不是我預期的數量。

若是不是,表示有人動過個人列表,拋異常,不幹了。

若是是,表示沒有人動過個人列表,才繼續執行。

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

解決方案

既然出現了問題,怎麼解決呢?

那就不要在遍歷的過程當中再去遞歸修改了,等他遍歷完再說。添加一個待處理的列表,遍歷以後再遞歸,保證每次迭代都不衝突。

logger.debug("下一次須要處理的父節點");
List<ClusterNode> nextParentNodes = new ArrayList<>();

logger.debug("遍歷列表");
Iterator<ClusterNode> iterator = clusterNodes.iterator();
while (iterator.hasNext()) {
    ClusterNode clusterNode = iterator.next();

    logger.debug("若是父節點匹配");
    if (clusterNode.getParentClusterNode().getName().equals(parentNode.getName())) {
        logger.debug("將當前節點添加到父節點的子列表中");
        parentNode.getClusterNodes().add(clusterNode);

        logger.debug("移除該節點");
        iterator.remove();

        logger.debug("添加到下一次父節點中");
        nextParentNodes.add(clusterNode);
    }
}

logger.debug("遞歸處理全部待處理的父節點");
for (ClusterNode clusterNode : nextParentNodes) {
    setChildrenNode(clusterNode, clusterNodes);
}

總結

迭代器並不是一勞永逸,保證屢次修改的獨立纔是最佳實踐。

相關文章
相關標籤/搜索