Java-迭代器引发-ConcurrentModificationException

39次阅读

共计 3431 个字符,预计需要花费 9 分钟才能阅读完成。

引言

ConcurrentModificationException这个异常大家都很熟悉,当在 forEach 进行删除时都会出现该异常。

如果你还不了解,请参考澍澍的博客:关于在 list 循环的过程中进行删除的处理 – 晨澍的博客

ConcurrentModificationException的解决方案之一是使用迭代器,但是不代表迭代器就一劳永逸了。

使用的时候还需斟酌数组的索引。

描述

问题场景

如下图所示:

原来的同步方法获取的节点是节点的父节点,根据父节点进行对应。

然而在同步更新文件的时候,发现这样并不好处理,在不改动原代码的情况下,设计了将列表转为树型结构的方法,这样可以从根节点向下开始遍历,便于操作。

也是在牛客网身经百战,实现这个难度不大。但在编写相关实现的时候,遇到了一个小问题。

迭代器智能吗?

第一步,将列表中的根节点找出来。

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

第二步,再从根节点开始,递归设置子节点。

/**
 * 为节点设置符合条件的子节点,同时递归,设置子节点的子节点
 * @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 异常,看来,迭代器还没有智能到如此地步。

源码

翻开 ArrayList 中迭代器的源码。(自从上次在慕课网的驱动下,强制自己阅读了 HashMap 的源码后,发现自己对读源码没那么抗拒了。)

在刷过编程题后,终于明白为什么这些家公司都问 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);
}

总结

迭代器并非一劳永逸,保证多次修改的独立才是最佳实践。

正文完
 0