對map,list等進(jìn)行遍歷的時(shí)候,不做增加闭树,刪除的時(shí)候,fori,fore,iterator等方式都沒太大區(qū)別荒澡,當(dāng)需要在遍歷的過程中报辱,增加或刪除元素的時(shí)候,就會(huì)遇到異常了单山。
具體參考這篇文章:
在foreach循環(huán)里進(jìn)行remove/add操作
具體的remove/add參考上面的文章就好碍现,接下來說一下我最近做的蠢事。
首先說一下錯(cuò)誤的做法米奸,在增強(qiáng)for循環(huán)里昼接,進(jìn)行add,和remove操作悴晰,會(huì)導(dǎo)致modCount和expectCount這2個(gè)值不一致慢睡。
我在用迭代器遍歷Map的時(shí)候,先iterator.remove,然后map.put最后拋了異常铡溪,一開始心想漂辐,用了迭代器進(jìn)行remove操作了,為什么還會(huì)拋這個(gè)異常呢棕硫,后來拍了拍腦袋髓涯,先remove,再add,也破壞了modCount和expectCount這2個(gè)值饲帅。
最后我用了2個(gè)數(shù)組复凳,保存了迭代過程中產(chǎn)生的局部遍量瘤泪,在退出循環(huán)之后灶泵,從2個(gè)數(shù)組取值育八,然后map.put,當(dāng)然赦邻,最后數(shù)組取值的 時(shí)候又做了點(diǎn)蠢事髓棋,數(shù)組下標(biāo)越界了。
+++++++++++++++++++++++我是分界線+++++++++++++++++++++++++
評論區(qū)有疑問惶洲,那就再補(bǔ)充一下細(xì)節(jié)按声。
貼代碼
測試代碼
先使用HashMap進(jìn)行測試
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("x",new Date());
map.put("y",new Date());
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> next = iterator.next();
System.out.println(next.getKey());
map.put("z",new Date());
}
}
根據(jù)控制臺(tái)測試結(jié)果,在輸出了第一個(gè)key
x
之后恬吕,繼續(xù)執(zhí)行的時(shí)候拋出了java.util.ConcurrentModificationException
異常签则。
我們查看異常堆棧,拋出異常的地方是java.util.HashMap.HashIterator#nextNode
,1445行
根據(jù)代碼铐料,拋出異常的原因是modCount != expectedModCount
渐裂。
這一行代碼說明拋異常的原因就是2個(gè)count不相等造成的。
變量解釋
來看一下modCount和expectedModCount具體代表什么意思钠惩。
modCount是HashMap的一個(gè)成員變量柒凉,用來表示HashMap被修改的次數(shù)
modCount值修改的時(shí)候,是在做put/remove等操作的時(shí)候
expectedModCount是HashMap的內(nèi)部類java.util.HashMap.HashIterator
的一個(gè)成員變量篓跛,初始化值為modCount
expectedModCount值修改的時(shí)候膝捞,除了初始化,只有一處調(diào)用:java.util.HashMap.HashIterator#remove
原因分析
前面看了代碼了愧沟,知道異常拋出的原因是modCount != expectedModCount
蔬咬。
看代碼,是使用迭代器遍歷map沐寺,在迭代的過程中執(zhí)行了put操作计盒,根據(jù)代碼可知,put操作是會(huì)改變modCount的值的芽丹,但put操作不會(huì)修改expectedModCount北启,expectedModCount的值只有在調(diào)用HashIterator#remove
方法的時(shí)候才會(huì)被修改。
所以只修改了一個(gè)值拔第,在執(zhí)行完一次put操作咕村,進(jìn)入下一個(gè)循環(huán)的時(shí)候就會(huì)拋出異常。
安全容器
評論區(qū)也提出了使用安全的容器不會(huì)有這個(gè)異常蚊俺。確實(shí)是這樣懈涛,將HashMap換成ConcurrentHashMap,不會(huì)拋出這個(gè)異常泳猬。
可以看到代碼完整的執(zhí)行完了批钠,并沒有拋出異常宇植。
關(guān)于安全容器,這里解釋2個(gè)概念
fail-fast
fail-fast埋心,即快速失敗指郁,它是Java集合的一種錯(cuò)誤檢測機(jī)制。當(dāng)多個(gè)線程對集合(非fail-safe的集合類)進(jìn)行結(jié)構(gòu)上的改變的操作時(shí)拷呆,有可能會(huì)產(chǎn)生fail-fast機(jī)制闲坎,這個(gè)時(shí)候就會(huì)拋出ConcurrentModificationException(當(dāng)方法檢測到對象的并發(fā)修改,但不允許這種修改時(shí)就拋出該異常)茬斧。
同時(shí)需要注意的是腰懂,即使不是多線程環(huán)境,如果單線程違反了規(guī)則项秉,同樣也有可能會(huì)拋出改異常绣溜。
HashMap不是并發(fā)安全的容器,所以采用的是fail-fast機(jī)制娄蔼。
剛才在看迭代器的源碼的時(shí)候怖喻,expectedModCount的注釋就寫了這個(gè)變量是為了fast-fail準(zhǔn)備的。
fail-safe
在Java中贷屎,除了一些普通的集合類以外罢防,還有一些采用了fail-safe機(jī)制的集合類。這樣的集合容器在遍歷時(shí)不是直接在集合內(nèi)容上訪問的唉侄,而是先復(fù)制原有集合內(nèi)容咒吐,在拷貝的集合上進(jìn)行遍歷。
由于迭代時(shí)是對原集合的拷貝進(jìn)行遍歷属划,所以在遍歷過程中對原集合所作的修改并不能被迭代器檢測到恬叹,所以不會(huì)觸發(fā)ConcurrentModificationException。
基于拷貝內(nèi)容的優(yōu)點(diǎn)是避免了ConcurrentModificationException同眯,但同樣地绽昼,迭代器并不能訪問到修改后的內(nèi)容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝须蜗,在遍歷期間原集合發(fā)生的修改迭代器是不知道的硅确。
java.util.concurrent包下的容器都是安全失敗,可以在多線程下并發(fā)使用明肮,并發(fā)修改菱农。
java.util.concurrent.ConcurrentHashMap是符合fail-safe語意的,但是并不是迭代是拷貝副本柿估,創(chuàng)建EntrySet的時(shí)候循未,傳入的是this,即修改的是自身集合
注釋也作了說明,map的修改會(huì)反映到Set上
The set is backed by the map, so changes to the map are *reflected in the set, and vice-versa
關(guān)于ConcurrentHashMap是否存在拷貝秫舌,stackoverflow上有一些問答的妖,供參考
總結(jié)
Java集合類設(shè)計(jì)時(shí)有2種保障機(jī)制绣檬,為不同場景下安全的使用集合提供了充分的保障。
我們開發(fā)時(shí)一般使用HashMap會(huì)更多嫂粟,需要并發(fā)集合的時(shí)候才會(huì)考慮使用ConcurrentHashMap娇未。
了解2種機(jī)制,可以更好的在實(shí)際開發(fā)過程中進(jìn)行應(yīng)用赋元。