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