Java集合快速失敗與安全失敗解析
Java集合快速失敗與安全失敗
前言
我們?cè)陂_(kāi)發(fā)過(guò)程中有沒(méi)有在遍歷集合的時(shí)候遇到過(guò)ConcurrentModificationException這樣的異常,那么什么樣的原因?qū)е逻@種異常呢?本篇博客將帶領(lǐng)大家去了解一下Java集合fail-fast快速失敗機(jī)制與fail-safe安全失敗機(jī)制。
正文
fail-fast與fail-safe
- fail-fast快速失敗機(jī)制: 是Java集合中的一種機(jī)制,在用迭代器遍歷一個(gè)集合對(duì)象時(shí),如果遍歷過(guò)程中對(duì)集合對(duì)象的內(nèi)容進(jìn)行了修改(增加、刪除、修改),則會(huì)拋出ConcurrentModificationException。
- fail-safe安全失敗機(jī)制:java.util.concurrent包下的容器都是安全失敗,在遍歷時(shí)不是直接在集合內(nèi)容上訪問(wèn)的,而是先copy原有集合內(nèi)容,在拷貝的集合上進(jìn)行遍歷,因此采用安全失敗的容器可以在多線程下并發(fā)使用,并發(fā)修改。
fail-fast快速失敗機(jī)制
public class test { public static void main(String[] args) { testForHashMap(); } private static void testForHashMap() { HashMap<String,String> hashMap =new LinkedHashMap<>(); hashMap.put("1","a"); hashMap.put("2","b"); hashMap.put("3","c"); Iterator<Map.Entry<String,String>> iterator=hashMap.entrySet().iterator(); while (iterator.hasNext()) { hashMap.put("bloom","bloom"); System.out.println(iterator.next()); } } }
快速失敗機(jī)制下修改集合元素觸發(fā)快速失敗,輸出結(jié)果:
遍歷集合時(shí),新增或者刪除元素,將拋ConcurrentModificationException異常
fail-safe安全失敗機(jī)制
public class test { public static void main(String[] args) { testForHashTable(); } private static void testForHashTable() { Hashtable<String,String> hashtable =new Hashtable(); hashtable.put("4","d"); hashtable.put("5","e"); hashtable.put("6","f"); Enumeration<String> iterator1=hashtable.elements(); while (iterator1.hasMoreElements()) { hashtable.put("bloom","bloom"); System.out.println(iterator1.nextElement()); } } }
安全失敗機(jī)制下修改集合元素,輸出結(jié)果
我們可以在遍歷集合的同時(shí),新增、刪除元素
小結(jié)一下
fail-fast,它是Java集合的一種錯(cuò)誤檢測(cè)機(jī)制。
在用迭代器遍歷一個(gè)集合對(duì)象時(shí),如果遍歷過(guò)程中不應(yīng)該對(duì)集合對(duì)象的內(nèi)容進(jìn)行了修改(增加、刪除、修改),可以新建一個(gè)新的集合進(jìn)行操作。
快速失敗&安全失?。ㄗ钊目偨Y(jié))
public static void main(String[] args) { Hashtable<String, String> table = new Hashtable<String, String>(); table.put("a", "aa"); table.put("b", "bb"); table.put("c", "cc"); table.remove("c"); Iterator<Entry<String, String>> iterator = table.entrySet().iterator(); while (iterator.hasNext()) { System.out.println(iterator.next().getValue()); //采用iterator直接進(jìn)行修改 程序正常 iterator.remove(); //直接從hashtable增刪數(shù)據(jù)就會(huì)報(bào)錯(cuò) table.put("d", "dd"); //直接從hashtable增刪數(shù)據(jù)就會(huì)報(bào)錯(cuò),hashtable,hashmap等非并發(fā)集合,如果在迭代過(guò)程中增減了數(shù)據(jù),就是快速失敗 table.remove("c"); } System.out.println("-----------"); Lock lock = new ReentrantLock(); //即使加上lock,還是會(huì)跑出ConcurrentModificationException異常 lock.lock(); HashMap<String, String> hashmap = new HashMap<String, String>(); hashmap.put("a", "aa"); hashmap.put("b", "bb"); hashmap.put("c", "cc"); Iterator<Entry<String, String>> iterators = hashmap.entrySet().iterator(); while (iterators.hasNext()) { System.out.println(iterators.next().getValue()); // 正常 iterators.remove(); //直接從hashtable增刪數(shù)據(jù)就會(huì)報(bào)錯(cuò)。 //hashtable,hashmap等非并發(fā)集合,如果在迭代過(guò)程中增減了數(shù)據(jù),會(huì)快速失敗 (一檢測(cè)到修改,馬上拋異常) //java.util.ConcurrentModificationException hashmap.remove("c"); } System.out.println("-----------"); lock.unlock(); ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>(); map.put("a", "aa"); map.put("b", "bb"); map.put("c", "cc"); Iterator<Entry<String, String>> mapiterator = map.entrySet().iterator(); while (mapiterator.hasNext()) { System.out.println(mapiterator.next().getValue()); map.remove("c");// 正常 并發(fā)集合不存在快速失敗問(wèn)題 map.put("c", "cc");// 正常 并發(fā)集合不存在快速失敗問(wèn)題 } System.out.println("-----------"); }
運(yùn)行該段代碼發(fā)現(xiàn),在Hashtable和HashMap的循環(huán)迭代過(guò)程中在容器對(duì)象上做“修改”操作的話,是跑出java.util.ConcurrentModificationException異常,在Iterator上做操作不會(huì)異常。但是ConcurrentHashMap在容器對(duì)象和Iterator對(duì)象上都不會(huì)拋異常,這是為什么呢?
(1)首先來(lái)介紹兩個(gè)概念,快速失敗和安全失敗。
Iterator的安全失敗是基于對(duì)底層集合做拷貝,因此,它不受源集合上修改的影響。java.util包下面的所有的集合類都是快速失敗的,而java.util.concurrent包下面的所有的類都是安全失敗的。
快速失敗的迭代器會(huì)拋出ConcurrentModificationException異常,而安全失敗的迭代器永遠(yuǎn)不會(huì)拋出這樣的異常。
(2)我們查看Hashtable、HashMap、ConcurrentHashMap的在Java API底層的entrySet對(duì)象發(fā)現(xiàn),三者都做了對(duì)當(dāng)前對(duì)象的拷貝,三者的處理方式是一樣的,那區(qū)別在哪里呢?看看獲取下一個(gè)entrySet在邏輯上的區(qū)別
這是Hashtable、HashMap的
final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; }
這是ConcurrentHashMap的
public final Map.Entry<K,V> next() { Node<K,V> p; if ((p = next) == null) throw new NoSuchElementException(); K k = p.key; V v = p.val; lastReturned = p; advance(); return new MapEntry<K,V>(k, v, map); } /** * Advances if possible, returning next valid node, or null if none. */ final Node<K,V> advance() { Node<K,V> e; if ((e = next) != null) e = e.next; for (;;) { Node<K,V>[] t; int i, n; // must use locals in checks if (e != null) return next = e; if (baseIndex >= baseLimit || (t = tab) == null || (n = t.length) <= (i = index) || i < 0) return next = null; if ((e = tabAt(t, i)) != null && e.hash < 0) { if (e instanceof ForwardingNode) { tab = ((ForwardingNode<K,V>)e).nextTable; e = null; pushState(t, i, n); continue; } else if (e instanceof TreeBin) e = ((TreeBin<K,V>)e).first; else e = null; } if (stack != null) recoverState(n); else if ((index = i + baseSize) >= n) index = ++baseIndex; // visit upper slots if present } }
ConcurrentHashMap中的迭代器主要包括entrySet、keySet、values方法。它們大同小異,這里選擇entrySet解釋。當(dāng)我們調(diào)用entrySet返回值的iterator方法時(shí),返回的是EntryIterator,在EntryIterator上調(diào)用next方法時(shí),最終實(shí)際調(diào)用到了HashIterator.advance()方法。這個(gè)方法在遍歷底層數(shù)組。
在遍歷過(guò)程中,如果已經(jīng)遍歷的數(shù)組上的內(nèi)容變化了,迭代器不會(huì)拋出ConcurrentModificationException異常。如果未遍歷的數(shù)組上的內(nèi)容發(fā)生了變化,則有可能反映到迭代過(guò)程中。
這就是ConcurrentHashMap迭代器弱一致的表現(xiàn)。ConcurrentHashMap的弱一致性主要是為了提升效率,是一致性與效率之間的一種權(quán)衡。要成為強(qiáng)一致性,就得到處使用鎖,甚至是全局鎖,這就與Hashtable和同步的HashMap一樣了。
最后我們看看JDK中對(duì)于快速失敗的描述:
注意,此實(shí)現(xiàn)不是同步的。如果多個(gè)線程同時(shí)訪問(wèn)一個(gè)哈希映射,而其中至少一個(gè)線程從結(jié)構(gòu)上修改了該映射,則它必須 保持外部同步。(結(jié)構(gòu)上的修改是指添加或刪除一個(gè)或多個(gè)映射關(guān)系的任何操作;僅改變與實(shí)例已經(jīng)包含的鍵關(guān)聯(lián)的值不是結(jié)構(gòu)上的修改。)這一般通過(guò)對(duì)自然封裝該映射的對(duì)象進(jìn)行同步操作來(lái)完成。
如果不存在這樣的對(duì)象,則應(yīng)該使用 Collections.synchronizedMap 方法來(lái)“包裝”該映射。最好在創(chuàng)建時(shí)完成這一操作,以防止對(duì)映射進(jìn)行意外的非同步訪問(wèn),如下所示: Map m = Collections.synchronizedMap(new HashMap(...));由所有此類的“collection 視圖方法”所返回的迭代器都是快速失敗 的:在迭代器創(chuàng)建之后,如果從結(jié)構(gòu)上對(duì)映射進(jìn)行修改,除非通過(guò)迭代器本身的 remove 方法,其他任何時(shí)間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。因此,面對(duì)并發(fā)的修改,迭代器很快就會(huì)完全失敗,而不冒在將來(lái)不確定的時(shí)間發(fā)生任意不確定行為的風(fēng)險(xiǎn)。
注意,迭代器的快速失敗行為不能得到保證,一般來(lái)說(shuō),存在非同步的并發(fā)修改時(shí),不可能作出任何堅(jiān)決的保證??焖偈〉鞅M最大努力拋出 ConcurrentModificationException。因此,編寫(xiě)依賴于此異常的程序的做法是錯(cuò)誤的,正確做法是:迭代器的快速失敗行為應(yīng)該僅用于檢測(cè)程序錯(cuò)誤。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳談Map的key、value值的數(shù)據(jù)類型不能為基本類型的原因
這篇文章主要介紹了詳談Map的key、value值的數(shù)據(jù)類型不能為基本類型的原因,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09SpringBoot如何配置獲取request中body的json格式參數(shù)
這篇文章主要介紹了SpringBoot如何配置獲取request中body的json格式參數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06前后端項(xiàng)目分離解決cors錯(cuò)誤的方法詳解
隨著前后端分離技術(shù)的越來(lái)越盛行,跨域問(wèn)題也逐漸凸顯了出來(lái),下面這篇文章主要給大家介紹了關(guān)于前后端項(xiàng)目分離解決cors錯(cuò)誤的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02SpringBoot整合Swagger Api自動(dòng)生成文檔的實(shí)現(xiàn)
本文主要介紹了SpringBoot整合Swagger Api自動(dòng)生成文檔的實(shí),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06特殊數(shù)據(jù)結(jié)構(gòu)之使用Java實(shí)現(xiàn)單調(diào)棧示例
這篇文章主要為大家介紹了特殊數(shù)據(jù)結(jié)構(gòu)之使用Java實(shí)現(xiàn)單調(diào)棧示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09