點贊再看,動力無限。Hello world : ) 微信搜「 程序猿阿朗 」。
本文 Github.com/niumoo/JavaNotes 和 未讀代碼博客 已經收錄,有很多知識點和系列文章。
前言
既然是繞過迭代器遍歷時的數據修改異常,那么有必要先看一下是什么樣的異常。如果在集合的迭代器遍歷時嘗試更新集合中的數據,比如像下面這樣,我想輸出 Hello,World,Java
,迭代時卻發現多了一個 C++
元素,如果直接刪除掉的話。
List<String> list = new ArrayList<>();
Collections.addAll(list, "Hello", "World", "C++", "Java");
// 我想輸出 Hello,World,Java,迭代時發現多一個 C++,所以直接刪除掉。
Iterator iterator = list.iterator();
System.out.println(iterator.next());
System.out.println(iterator.next());
list.remove("C++");
System.out.println(iterator.next());
那么我想你一定會遇到一個異常 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)
這個異常在剛開始學習 Java 或者使用其他的非線程安全的集合過程中可能都有遇到過。導致這個報錯出現的原因就和我們操作的一樣,對於某些集合,不建議在遍歷時進行數據修改,因為這樣會數據出現不確定性。
那么如何繞過這個錯誤呢?這篇文章中腦洞大開的三種方式一定不會讓你失望。
異常原因
這不是一篇源碼分析的文章,但是為了介紹繞過這個異常出現的原因,還是要提一下的,已經知道的同學可以直接跳過。
根據上面的報錯,可以追蹤到報錯位置 ArrayList.java
的 857 行和 907 行,追蹤源碼可以發現在迭代器的 next 方法的第一行,調用了 checkForComodification()
方法。
而這個方法直接進行了一個把變量 modCount
和 expectedModCount
進行了對比,如果不一致就會拋出來 ConcurrentModificationException
異常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
那么 modCount
這個變量存儲的是什么信息呢?
/**
* 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;
直接看源碼注釋吧,直接翻譯一下意思就是說 modCount
數值記錄的是列表的結構被修改的次數,結構修改是指那些改變列表大小的修改,或者以某種方式擾亂列表,從而使得正在進行的迭代可能產生不正確的結果。同時也指出了這個字段通常會在迭代器 iterator 和 listIterator 返回的結果中使用,如果 modCount
和預期的值不一樣,會拋出 ConcurrentModificationException
異常。
而上面與 modCount
進行對比的字段 expectedModCount
的值,其實是在創建迭代器時,從 modCount
獲取的值。如果列表結構沒有被修改過,那么兩者的值應該是一致的。
繞過方式一:40 多億次循環繞過
上面分析了異常產生的位置和原因,是因為 modCount
的當前值和創建迭代器時的值有所變化。所以第一種思路很簡單,我們只要能讓兩者的值一致就可以了。在源碼 int modCount = 0;
中可以看到 modCount
的數據類型是 INT ,既然是 INT ,就是有數據范圍,每次更新列表結構 modCount
都會增1,那么是不是可以增加到 INT 數據類型的值的最大值溢出到負數,再繼續增加直到變回原來的值呢?如果可以這樣,首先要有一種操作可以在更新列表結構的同時不修改數據。為此翻閱了源碼尋找這樣的方法。還真的存在這樣的方法。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
上來就遞增了 modCount
,同時沒有修改任何數據,只是把數據的存儲進行了壓縮。
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 多億次遍歷,溢出到負數,繼續溢出到原值
for (int n = Integer.MIN_VALUE; n < Integer.MAX_VALUE; n++) ((ArrayList) list).trimToSize();
System.out.println(iterator.next());
正確輸出了想要的 Hello,World,Java
。
繞過方式二:線程加對象鎖繞過
分析一下我們的代碼,每次輸出的都是 System.out.println(iterator.next());
。可以看出來是先運行了迭代器 next
方法,然后才運行了System.out
進行輸出。所以第二種思路是先把第三個元素C++
更新為Java
,然后啟動一個線程,在迭代器再次調用 next
方法后,把第四個元素移除掉。這樣就輸出了我們想要的結果。
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.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
。這里簡單說一下代碼中的思路,Phaser 是 JDK 7 的新增類,是一個階段執行處理器。構造時的參數 parties 的值為2,說明需要兩個參與方完成時才會進行到下一個階段。而 arriveAndAwaitAdvance
方法被調用時,可以讓一個參與方到達。
所以線程中對 System.out
進行加鎖,然后執行 arriveAndAwaitAdvance
使一個參與方報告完成,此時會阻塞,等到另一個參與方報告完成后,線程進入到一個主線程不為阻塞狀態時的循環。
這時主線程執行 System.out.println(iterator.next());
。獲取到迭代器的值進行輸出時,因為線程內的加鎖原因,主線程會被阻塞。知道線程內把集合的最后一個元素移除,線程處理完成才會繼續。
繞過方式三:利用類型擦除放入魔法對象
在創建集合的時候為了減少錯誤概率,我們會使用泛型限制放入的數據類型,其實呢,泛型限制的集合在運行時也是沒有限制的,我們可以放入任何對象。所以我們可以利用這一點做些文章。
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)list).set(2, new Object() {
public String toString() {
String s = list.get(3);
list.remove(this);
return s;
}
});
System.out.println(iterator.next());
代碼里直接把第三個元素放入了一個魔法對象,重寫了 toString()
方法,內容是返回集合的第四個元素,然后刪除第三個元素,這樣就可以得到想要的 Hello,World,Java
輸出。
上面就是繞過迭代器遍歷時的數據修改報錯的三種方法了,不管實用性如何,我覺得每一種都是大開腦洞的操作,這些操作都需要對某個知識點有一定的了解,關注我,了解更多稀奇古怪的開發技巧。
參考:
[1] https://www.javaspecialists.eu/archive/Issue186-Iterator-Quiz.html
<完>
Hello world : ) 我是阿朗,一線技術工具人,認認真真寫文章。
點贊的個個都是人才,不僅長得帥氣好看,說話還好聽。
文章持續更新,可以關注公眾號「 程序猿阿朗 」或訪問「未讀代碼博客 」。
回復【資料】有我准備的各系列知識點和必看書籍。
本文 Github.com/niumoo/JavaNotes 已經收錄,有很多知識點和系列文章,歡迎Star。