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); }
迭代器並不是一勞永逸,保證屢次修改的獨立纔是最佳實踐。