Java 如何繞過(guò)迭代器遍歷時(shí)的數(shù)據(jù)修改異常
前言
既然是繞過(guò)迭代器遍歷時(shí)的數(shù)據(jù)修改異常,那么有必要先看一下是什么樣的異常。如果在集合的迭代器遍歷時(shí)嘗試更新集合中的數(shù)據(jù),比如像下面這樣,我想輸出 Hello,World,Java,迭代時(shí)卻發(fā)現(xiàn)多了一個(gè) C++ 元素,如果直接刪除掉的話(huà)。
List<String> list = new ArrayList<>(); Collections.addAll(list, "Hello", "World", "C++", "Java"); // 我想輸出 Hello,World,Java,迭代時(shí)發(fā)現(xiàn)多一個(gè) C++,所以直接刪除掉。 Iterator iterator = list.iterator(); System.out.println(iterator.next()); System.out.println(iterator.next()); list.remove("C++"); System.out.println(iterator.next());
那么我想你一定會(huì)遇到一個(gè)異常 ConcurrentModificationExceptio
。
Hello World java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:907) at java.util.ArrayList$Itr.next(ArrayList.java:857) at com.wdbyte.lab.jdk.ModCountDemo.updateCollections(ModCountDemo.java:26)
這個(gè)異常在剛開(kāi)始學(xué)習(xí) Java 或者使用其他的非線(xiàn)程安全的集合過(guò)程中可能都有遇到過(guò)。導(dǎo)致這個(gè)報(bào)錯(cuò)出現(xiàn)的原因就和我們操作的一樣,對(duì)于某些集合,不建議在遍歷時(shí)進(jìn)行數(shù)據(jù)修改,因?yàn)檫@樣會(huì)數(shù)據(jù)出現(xiàn)不確定性。
那么如何繞過(guò)這個(gè)錯(cuò)誤呢?這篇文章中腦洞大開(kāi)的三種方式一定不會(huì)讓你失望。
異常原因
這不是一篇源碼分析的文章,但是為了介紹繞過(guò)這個(gè)異常出現(xiàn)的原因,還是要提一下的,已經(jīng)知道的同學(xué)可以直接跳過(guò)。
根據(jù)上面的報(bào)錯(cuò),可以追蹤到報(bào)錯(cuò)位置 ArrayList.java 的 857 行和 907 行,追蹤源碼可以發(fā)現(xiàn)在迭代器的 next 方法的第一行,調(diào)用了 checkForComodification() 方法。
而這個(gè)方法直接進(jìn)行了一個(gè)把變量 modCount
和 expectedModCount
進(jìn)行了對(duì)比,如果不一致就會(huì)拋出來(lái) ConcurrentModificationException
異常。
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
那么 modCount
這個(gè)變量存儲(chǔ)的是什么信息呢?
/** * The number of times this list has been <i>structurally modified</i>. * Structural modifications are those that change the size of the * list, or otherwise perturb it in such a fashion that iterations in * progress may yield incorrect results. * * <p>This field is used by the iterator and list iterator implementation * returned by the {@code iterator} and {@code listIterator} methods. * If the value of this field changes unexpectedly, the iterator (or list * iterator) will throw a {@code ConcurrentModificationException} in * response to the {@code next}, {@code remove}, {@code previous}, * {@code set} or {@code add} operations. This provides * <i>fail-fast</i> behavior, rather than non-deterministic behavior in * the face of concurrent modification during iteration. * * <p><b>Use of this field by subclasses is optional.</b> If a subclass * wishes to provide fail-fast iterators (and list iterators), then it * merely has to increment this field in its {@code add(int, E)} and * {@code remove(int)} methods (and any other methods that it overrides * that result in structural modifications to the list). A single call to * {@code add(int, E)} or {@code remove(int)} must add no more than * one to this field, or the iterators (and list iterators) will throw * bogus {@code ConcurrentModificationExceptions}. If an implementation * does not wish to provide fail-fast iterators, this field may be * ignored. */ protected transient int modCount = 0;
直接看源碼注釋吧,直接翻譯一下意思就是說(shuō) modCount 數(shù)值記錄的是列表的結(jié)構(gòu)被修改的次數(shù),結(jié)構(gòu)修改是指那些改變列表大小的修改,或者以某種方式擾亂列表,從而使得正在進(jìn)行的迭代可能產(chǎn)生不正確的結(jié)果。同時(shí)也指出了這個(gè)字段通常會(huì)在迭代器 iterator 和 listIterator 返回的結(jié)果中使用,如果 modCount 和預(yù)期的值不一樣,會(huì)拋出 ConcurrentModificationException 異常。
而上面與 modCount 進(jìn)行對(duì)比的字段 expectedModCount 的值,其實(shí)是在創(chuàng)建迭代器時(shí),從 modCount 獲取的值。如果列表結(jié)構(gòu)沒(méi)有被修改過(guò),那么兩者的值應(yīng)該是一致的。
繞過(guò)方式一:40 多億次循環(huán)繞過(guò)
上面分析了異常產(chǎn)生的位置和原因,是因?yàn)?modCount 的當(dāng)前值和創(chuàng)建迭代器時(shí)的值有所變化。所以第一種思路很簡(jiǎn)單,我們只要能讓兩者的值一致就可以了。在源碼 int modCount = 0; 中可以看到 modCount 的數(shù)據(jù)類(lèi)型是 INT ,既然是 INT ,就是有數(shù)據(jù)范圍,每次更新列表結(jié)構(gòu) modCount 都會(huì)增1,那么是不是可以增加到 INT 數(shù)據(jù)類(lèi)型的值的最大值溢出到負(fù)數(shù),再繼續(xù)增加直到變回原來(lái)的值呢?如果可以這樣,首先要有一種操作可以在更新列表結(jié)構(gòu)的同時(shí)不修改數(shù)據(jù)。為此翻閱了源碼尋找這樣的方法。還真的存在這樣的方法。
public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
上來(lái)就遞增了 modCount
,同時(shí)沒(méi)有修改任何數(shù)據(jù),只是把數(shù)據(jù)的存儲(chǔ)進(jìn)行了壓縮。
List<String> list = new ArrayList<>(); Collections.addAll(list, "Hello", "World", "C++", "Java"); list.listIterator(); Iterator iterator = list.iterator(); System.out.println(iterator.next()); System.out.println(iterator.next()); list.remove("C++"); // 40 多億次遍歷,溢出到負(fù)數(shù),繼續(xù)溢出到原值 for (int n = Integer.MIN_VALUE; n < Integer.MAX_VALUE; n++) ((ArrayList) list).trimToSize(); System.out.println(iterator.next());
正確輸出了想要的 Hello,World,Java 。
繞過(guò)方式二:線(xiàn)程加對(duì)象鎖繞過(guò)
分析一下我們的代碼,每次輸出的都是 System.out.println(iterator.next());。可以看出來(lái)是先運(yùn)行了迭代器 next 方法,然后才運(yùn)行了System.out 進(jìn)行輸出。所以第二種思路是先把第三個(gè)元素C++ 更新為Java ,然后啟動(dòng)一個(gè)線(xiàn)程,在迭代器再次調(diào)用 next 方法后,把第四個(gè)元素移除掉。這樣就輸出了我們想要的結(jié)果。
List<String> list = new ArrayList<>(); Collections.addAll(list, "Hello", "World", "C++", "Java"); list.listIterator(); Iterator iterator = list.iterator(); System.out.println(iterator.next()); System.out.println(iterator.next()); // 開(kāi)始操作 list.set(2, "Java"); Phaser phaser = new Phaser(2); Thread main = Thread.currentThread(); new Thread(() -> { synchronized (System.out) { phaser.arriveAndDeregister(); while (main.getState() != State.BLOCKED) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } list.remove(3); } }).start(); phaser.arriveAndAwaitAdvance(); System.out.println(iterator.next()); // 輸出集合 System.out.println(list); /** * 得到輸出 * * Hello * World * Java * [Hello, World, Java] */
正確輸出了想要的 Hello,World,Java
。這里簡(jiǎn)單說(shuō)一下代碼中的思路,Phaser 是 JDK 7 的新增類(lèi),是一個(gè)階段執(zhí)行處理器。構(gòu)造時(shí)的參數(shù) parties 的值為2,說(shuō)明需要兩個(gè)參與方完成時(shí)才會(huì)進(jìn)行到下一個(gè)階段。而 arriveAndAwaitAdvance
方法被調(diào)用時(shí),可以讓一個(gè)參與方到達(dá)。
所以線(xiàn)程中對(duì) System.out
進(jìn)行加鎖,然后執(zhí)行 arriveAndAwaitAdvance
使一個(gè)參與方報(bào)告完成,此時(shí)會(huì)阻塞,等到另一個(gè)參與方報(bào)告完成后,線(xiàn)程進(jìn)入到一個(gè)主線(xiàn)程不為阻塞狀態(tài)時(shí)的循環(huán)。
這時(shí)主線(xiàn)程執(zhí)行 System.out.println(iterator.next());
。獲取到迭代器的值進(jìn)行輸出時(shí),因?yàn)榫€(xiàn)程內(nèi)的加鎖原因,主線(xiàn)程會(huì)被阻塞。知道線(xiàn)程內(nèi)把集合的最后一個(gè)元素移除,線(xiàn)程處理完成才會(huì)繼續(xù)。
繞過(guò)方式三:利用類(lèi)型擦除放入魔法對(duì)象
在創(chuàng)建集合的時(shí)候?yàn)榱藴p少錯(cuò)誤概率,我們會(huì)使用泛型限制放入的數(shù)據(jù)類(lèi)型,其實(shí)呢,泛型限制的集合在運(yùn)行時(shí)也是沒(méi)有限制的,我們可以放入任何對(duì)象。所以我們可以利用這一點(diǎn)做些文章。
List<String> list = new ArrayList<>(); Collections.addAll(list, "Hello", "World", "C++", "Java"); list.listIterator(); Iterator iterator = list.iterator(); System.out.println(iterator.next()); System.out.println(iterator.next()); // 開(kāi)始操作 ((List)list).set(2, new Object() { public String toString() { String s = list.get(3); list.remove(this); return s; } }); System.out.println(iterator.next());
代碼里直接把第三個(gè)元素放入了一個(gè)魔法對(duì)象,重寫(xiě)了 toString() 方法,內(nèi)容是返回集合的第四個(gè)元素,然后刪除第三個(gè)元素,這樣就可以得到想要的 Hello,World,Java 輸出。
上面就是繞過(guò)迭代器遍歷時(shí)的數(shù)據(jù)修改報(bào)錯(cuò)的三種方法了,不管實(shí)用性如何,我覺(jué)得每一種都是大開(kāi)腦洞的操作,這些操作都需要對(duì)某個(gè)知識(shí)點(diǎn)有一定的了解
以上就是Java 如何繞過(guò)迭代器遍歷時(shí)的數(shù)據(jù)修改異常的詳細(xì)內(nèi)容,更多關(guān)于Java 遍歷時(shí)的數(shù)據(jù)修改異常的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MybatisPlus實(shí)現(xiàn)數(shù)據(jù)攔截的使用示例
在MyBatis-Plus中,可以通過(guò)自定義攔截器來(lái)實(shí)現(xiàn)對(duì)SQL語(yǔ)句的攔截和修改,本文就來(lái)介紹一下如何使用,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10java的socket請(qǐng)求和響應(yīng)方式
這篇文章主要介紹了java的socket請(qǐng)求和響應(yīng)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09java實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07實(shí)現(xiàn)一個(gè)簡(jiǎn)單Dubbo完整過(guò)程詳解
這篇文章主要為大家介紹了實(shí)現(xiàn)一個(gè)簡(jiǎn)單Dubbo完整過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07