ConcurrentModificationException
这个异常你们都很熟悉,当在forEach
进行删除时都会出现该异常。java
若是你还不了解,请参考澍澍的博客:关于在list循环的过程当中进行删除的处理 - 晨澍的博客git
ConcurrentModificationException
的解决方案之一是使用迭代器,可是不表明迭代器就一劳永逸了。github
使用的时候还需斟酌数组的索引。算法
以下图所示:编程
原来的同步方法获取的节点是节点的父节点,根据父节点进行对应。数组
然而在同步更新文件的时候,发现这样并很差处理,在不改动原代码的状况下,设计了将列表转为树型结构的方法,这样能够从根节点向下开始遍历,便于操做。安全
也是在牛客网身经百战,实现这个难度不大。但在编写相关实现的时候,遇到了一个小问题。多线程
第一步,将列表中的根节点找出来。并发
@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
异常,看来,迭代器尚未智能到如此地步。
翻开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); }
迭代器并不是一劳永逸,保证屡次修改的独立才是最佳实践。