解析Java的迭代器中的fast-fail錯(cuò)誤檢測(cè)機(jī)制
fail-fast 機(jī)制是java集合(Collection)中的一種錯(cuò)誤機(jī)制。當(dāng)多個(gè)線程對(duì)同一個(gè)集合的內(nèi)容進(jìn)行操作時(shí),就可能會(huì)產(chǎn)生fail-fast事件。例如:當(dāng)某一個(gè)線程A通過(guò)iterator去遍歷某集合的過(guò)程中,若該集合的內(nèi)容被其他線程所改變了;那么線程A訪問(wèn)集合時(shí),就會(huì)拋出ConcurrentModificationException異常,產(chǎn)生fail-fast事件。
fail-fast 機(jī)制是java集合(Collection)中的一種錯(cuò)誤機(jī)制。當(dāng)多個(gè)線程對(duì)同一個(gè)集合的內(nèi)容進(jìn)行操作時(shí),就可能會(huì)產(chǎn)生fail-fast事件。
例如:當(dāng)某一個(gè)線程A通過(guò)iterator去遍歷某集合的過(guò)程中,若該集合的內(nèi)容被其他線程所改變了;那么線程A訪問(wèn)集合時(shí),就會(huì)拋出ConcurrentModificationException異常,產(chǎn)生fail-fast事件。
要了解fail-fast機(jī)制,我們首先要對(duì)ConcurrentModificationException 異常有所了解。當(dāng)方法檢測(cè)到對(duì)象的并發(fā)修改,但不允許這種修改時(shí)就拋出該異常。同時(shí)需要注意的是,該異常不會(huì)始終指出對(duì)象已經(jīng)由不同線程并發(fā)修改,如果單線程違反了規(guī)則,同樣也有可能會(huì)拋出改異常。
誠(chéng)然,迭代器的快速失敗行為無(wú)法得到保證,它不能保證一定會(huì)出現(xiàn)該錯(cuò)誤,但是快速失敗操作會(huì)盡最大努力拋出ConcurrentModificationException異常,所以因此,為提高此類操作的正確性而編寫(xiě)一個(gè)依賴于此異常的程序是錯(cuò)誤的做法,正確做法是:ConcurrentModificationException 應(yīng)該僅用于檢測(cè) bug。
Java中的Iterator非常方便地為所有的數(shù)據(jù)源提供了一個(gè)統(tǒng)一的數(shù)據(jù)讀取(刪除)的接口,但是新手通常在使用的時(shí)候容易報(bào)如下錯(cuò)誤ConcurrentModificationException,原因是在使用迭代器時(shí)候底層數(shù)據(jù)被修改,最常見(jiàn)于數(shù)據(jù)源不是線程安全的類,如HashMap & ArrayList等。
為什么要有fast-fail
一個(gè)案例
來(lái)一個(gè)新手容易犯錯(cuò)的例子:
String[] stringArray = {"a","b","c","d"}; List<String> strings = Arrays.asList(stringArray); Iterator<String> iterator = strings.iterator(); while (iterator.hasNext()) { if(iterator.next().equals("c")) { strings.remove("c"); } }
更加常見(jiàn)的是在foreach(本質(zhì)一樣,都是調(diào)用Iterator時(shí),操作了原始的strings)語(yǔ)句中:
for(String s : strings) { if(s.equals("c")) { strings.remove("c"); } }
產(chǎn)生原因
Java中的集合類(數(shù)據(jù)源)分為兩種類型:線程安全,位于java.util.concurrent命名目錄下,如CopyOnWriteArrayList;線程不安全:位于java.util目錄下,如ArrayList,HashMap。所謂線程安全是在多線程環(huán)境下,這個(gè)類還能表現(xiàn)出和行為規(guī)范一致的結(jié)果,是否文縐縐的...自己google吧。
那既然我們可以有線程安全的集合替代品,那么為什么還要存在ArrayList等呢?因?yàn)榫€程安全的類通常需要通過(guò)各種手段去保持對(duì)數(shù)據(jù)訪問(wèn)的同步,所以通常來(lái)說(shuō)效率會(huì)比較差。而如果使用者清楚自身使用場(chǎng)景不存在并發(fā)的場(chǎng)景,那么使用非線程安全的集合類在速度上有很大的優(yōu)勢(shì)。
如果開(kāi)發(fā)者在使用時(shí)沒(méi)有注意,將非線程安全的集合類用在了并發(fā)的場(chǎng)景下,比如線程A獲取了ArrayList的iterator,然后線程B通過(guò)調(diào)用ArrayList.add()修改了ArrayList的數(shù)據(jù),此時(shí)就有可能會(huì)拋出ConcurrentModificationException,注意,這里是有可能。那為啥上面的例子里面也會(huì)報(bào)這個(gè)錯(cuò)誤呢?上面并不存在并發(fā)的情況,摟一眼源碼吧。
Iterator源碼分析
集合類中的fast-fail實(shí)現(xiàn)方式都差不多,我們以最簡(jiǎn)單的ArrayList為例吧。
ArrayList中會(huì)持有一個(gè)變量,聲明為:
protected transient int modCount = 0;記錄的是我們對(duì)ArrayList修改的次數(shù),比如我們調(diào)用 add(),remove()等改變數(shù)據(jù)的操作時(shí),會(huì)將modCount++。
我們通過(guò)ArrayList.iterator()返回的是一個(gè)實(shí)現(xiàn)了Iterator接口的ArrayListIterator:
private class ArrayListIterator implements Iterator<E> { //省略部分代碼....... //初始化時(shí),直接給expectedModCount賦ArrayList的修改次數(shù) private int expectedModCount = modCount; @SuppressWarnings("unchecked") public E next() { ............ ArrayList<E> ourList = ArrayList.this; //簡(jiǎn)單比較一下當(dāng)前iterator初始化時(shí)ArrayList.modCount的值 //和現(xiàn)在的值是否一致,如果不相等,認(rèn)為在獲取了當(dāng)前iterator之后 //有別的位置(有可能是別的線程)修改了ArrayList,直接拋異常 if (ourList.modCount != expectedModCount) { throw new ConcurrentModificationException(); } ............ } }
原理很簡(jiǎn)單,構(gòu)建Iterator時(shí)將當(dāng)前ArrayList的modCount存起來(lái),以后每一次next()時(shí),判斷ArrayList的modCount值是否有變化,如果有,則是在這個(gè)過(guò)程中有代碼改變了數(shù)據(jù)(前面已經(jīng)提及,只有調(diào)用add() remove()等才會(huì)去修改modCount的值)。
這也說(shuō)明了為什么在例子里面我們并不是并發(fā)的場(chǎng)景也報(bào)錯(cuò),因?yàn)槲覀冋{(diào)用ArrayList.remove()時(shí)改變了modCount的值。
但是這個(gè)東西意義有多大呢?在我看來(lái)它有點(diǎn)畫(huà)蛇添足的嫌疑。因?yàn)樵谡嬲牟l(fā)場(chǎng)景下,這個(gè)fast-fail機(jī)制并不能真正即使發(fā)現(xiàn)另外線程訪問(wèn)并修改ArrayList中的數(shù)據(jù)。原因如下:
再看看modCount的定義protected transient int modCount = 0;。你沒(méi)有看錯(cuò),它就是一個(gè)普通的變量,那么在并發(fā)場(chǎng)景下由于共享對(duì)象的不可見(jiàn)性,有可能別的線程修改了ArrayList中的modCount,而iterator所在的線程卻并沒(méi)有讀取到這個(gè)更新。HashMap在1.6以前確實(shí)是用了volatile來(lái)修飾了modCount來(lái)保證各個(gè)線程直接對(duì)modCount的可見(jiàn)性,但是在1.7里面把這個(gè)修飾去掉了,而且認(rèn)為這是一個(gè)bug-->Java7去掉volatitle,可悲啊。。。原因嘛,就是JDK的開(kāi)發(fā)者認(rèn)為為了這么個(gè)破事而需要使用volatitle簡(jiǎn)直浪費(fèi)效率。
就算是使用volatitle就完事大吉了嗎?nono,舉個(gè)最簡(jiǎn)單的例子,線程A獲取了一個(gè)集合類的Iterator,線程B調(diào)用了集合類的add(),在add()還沒(méi)有執(zhí)行到modCount++時(shí),線程A獲取執(zhí)行,并執(zhí)行結(jié)束。在這種場(chǎng)景下,執(zhí)行結(jié)果并不確定。對(duì)于ArrayList的Iterator來(lái)說(shuō),有可能會(huì)報(bào)一個(gè)數(shù)組越界的異常...
總結(jié)
fast-fail是JDK為了提示開(kāi)發(fā)者將非線程安全的類使用到并發(fā)的場(chǎng)景下時(shí),拋出一個(gè)異常,及早發(fā)現(xiàn)代碼中的問(wèn)題。但正如本文前面所述,這種機(jī)制卻不能絕對(duì)正確地給出提示,而且老的JDK版本為了更好地支持這個(gè)機(jī)制還付出了一定的效率代價(jià)。
fast-fail存在的唯一價(jià)值可能就是給新手制造一些迷惑,給他深入探索的動(dòng)力...嘿嘿
補(bǔ)充:
很多網(wǎng)上資料說(shuō)在使用Iterator時(shí)是不能修改數(shù)據(jù)的,這樣也并不完全準(zhǔn)確。即便是支持fast-fail的Iterator本身也提供了remove()來(lái)刪除當(dāng)前遍歷到的元素,例如:ArrayListIterator中的remove(),前面舉的栗子改成如下即可:
while (iterator.hasNext()) { if(iterator.next().equals("c")) { iterator.remove("c"); } }
相關(guān)文章
android webview 中l(wèi)ocalStorage無(wú)效的解決方法
這篇文章主要介紹了android webview 中l(wèi)ocalStorage無(wú)效的解決方法,本文直接給出解決方法實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-06-06Android實(shí)現(xiàn)語(yǔ)音合成與識(shí)別功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)語(yǔ)音合成與識(shí)別功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07Android獲取熱點(diǎn)主機(jī)ip和連接熱點(diǎn)手機(jī)ip的代碼
這篇文章主要介紹了Android獲取熱點(diǎn)主機(jī)ip和連接熱點(diǎn)手機(jī)ip的相關(guān)資料,需要的朋友可以參考下2018-01-01Android中阻止AlertDialog關(guān)閉實(shí)例代碼
這篇文章主要介紹了Android阻止AlertDialog關(guān)閉實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-03-03Android實(shí)現(xiàn)可拖拽帶有坐標(biāo)尺進(jìn)度條的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用Android實(shí)現(xiàn)可拖拽帶有坐標(biāo)尺進(jìn)度條的效果,文中的示例代碼講解詳細(xì),需要的小伙伴可以參考一下2023-06-06Android下使用TCPDUMP實(shí)現(xiàn)數(shù)據(jù)抓包教程
這篇文章主要介紹了Android下使用TCPDUMP實(shí)現(xiàn)數(shù)據(jù)抓包教程,本文講解使用抓包工具tcpdump抓取數(shù)據(jù),然后使用Wireshark來(lái)分析數(shù)據(jù),需要的朋友可以參考下2015-02-02