一不小心就讓Java開發(fā)踩坑的fail-fast是個什么鬼?(推薦)
我在《為什么阿里巴巴禁止在 foreach 循環(huán)里進行元素的 remove/add 操作》一文中曾經(jīng)介紹過Java中的fail-fast機制,但是并沒有深入介紹,本文,就來深入介紹一下fail-fast。
什么是fail-fast
首先我們看下維基百科中關(guān)于fail-fast的解釋:
In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.
大概意思是:在系統(tǒng)設(shè)計中,快速失效系統(tǒng)一種可以立即報告任何可能表明故障的情況的系統(tǒng)??焖偈到y(tǒng)通常設(shè)計用于停止正常操作,而不是試圖繼續(xù)可能存在缺陷的過程。這種設(shè)計通常會在操作中的多個點檢查系統(tǒng)的狀態(tài),因此可以及早檢測到任何故障??焖偈∧K的職責(zé)是檢測錯誤,然后讓系統(tǒng)的下一個最高級別處理錯誤。
其實,這是一種理念,說白了就是在做系統(tǒng)設(shè)計的時候先考慮異常情況,一旦發(fā)生異常,直接停止并上報。
舉一個最簡單的fail-fast的例子:
public int divide(int divisor,int dividend){ if(dividend == 0){ throw new RuntimeException("dividend can't be null"); } return divisor/dividend; }
上面的代碼是一個對兩個整數(shù)做除法的方法,在divide方法中,我們對被除數(shù)做了個簡單的檢查,如果其值為0,那么就直接拋出一個異常,并明確提示異常原因。這其實就是fail-fast理念的實際應(yīng)用。
這樣做的好處就是可以預(yù)先識別出一些錯誤情況,一方面可以避免執(zhí)行復(fù)雜的其他代碼,另外一方面,這種異常情況被識別之后也可以針對性的做一些單獨處理。
怎么樣,現(xiàn)在你知道fail-fast了吧,其實他并不神秘,你日常的代碼中可能經(jīng)常會在使用的。
既然,fail-fast是一種比較好的機制,為什么文章標(biāo)題說fail-fast會有坑呢?
原因是Java的集合類中運用了fail-fast機制進行設(shè)計,一旦使用不當(dāng),觸發(fā)fail-fast機制設(shè)計的代碼,就會發(fā)生非預(yù)期情況。
集合類中的fail-fast
我們通常說的Java中的fail-fast機制,默認(rèn)指的是Java集合的一種錯誤檢測機制。當(dāng)多個線程對部分集合進行結(jié)構(gòu)上的改變的操作時,有可能會產(chǎn)生fail-fast機制,這個時候就會拋出ConcurrentModificationException(后文用CME代替)。
CMException,當(dāng)方法檢測到對象的并發(fā)修改,但不允許這種修改時就拋出該異常。
很多時候正是因為代碼中拋出了CMException,很多程序員就會很困惑,明明自己的代碼并沒有在多線程環(huán)境中執(zhí)行,為什么會拋出這種并發(fā)有關(guān)的異常呢?這種情況在什么情況下才會拋出呢?我們就來深入分析一下。
異常復(fù)現(xiàn)
在Java中, 如果在foreach 循環(huán)里對某些集合元素進行元素的 remove/add 操作的時候,就會觸發(fā)fail-fast機制,進而拋出CMException。
如以下代碼:
List<String> userNames = new ArrayList<String>() {{ add("Hollis"); add("hollis"); add("HollisChuang"); add("H"); }}; for (String userName : userNames) { if (userName.equals("Hollis")) { userNames.remove(userName); } } System.out.println(userNames);
以上代碼,使用增強for循環(huán)遍歷元素,并嘗試刪除其中的Hollis字符串元素。運行以上代碼,會拋出以下異常:
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at com.hollis.ForEach.main(ForEach.java:22)
同樣的,讀者可以嘗試下在增強for循環(huán)中使用add方法添加元素,結(jié)果也會同樣拋出該異常。
在深入原理之前,我們先嘗試把foreach進行解語法糖,看一下foreach具體如何實現(xiàn)的。
我們使用jad工具,對編譯后的class進行反編譯,得到以下代碼:
public static void main(String[] args) { // 使用ImmutableList初始化一個List List<String> userNames = new ArrayList<String>() {{ add("Hollis"); add("hollis"); add("HollisChuang"); add("H"); }}; Iterator iterator = userNames.iterator(); do { if(!iterator.hasNext()) break; String userName = (String)iterator.next(); if(userName.equals("Hollis")) userNames.remove(userName); } while(true); System.out.println(userNames); }
可以發(fā)現(xiàn),foreach其實是依賴了while循環(huán)和Iterator實現(xiàn)的。
異常原理
通過以上代碼的異常堆棧,我們可以跟蹤到真正拋出異常的代碼是:
java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
該方法是在iterator.next()方法中調(diào)用的。我們看下該方法的實現(xiàn):
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
如上,在該方法中對modCount和expectedModCount進行了比較,如果二者不想等,則拋出CMException。
那么,modCount和expectedModCount是什么?是什么原因?qū)е滤麄兊闹挡幌氲鹊哪兀?/p>
modCount是ArrayList中的一個成員變量。它表示該集合實際被修改的次數(shù)。
List<String> userNames = new ArrayList<String>() {{ add("Hollis"); add("hollis"); add("HollisChuang"); add("H"); }};
當(dāng)使用以上代碼初始化集合之后該變量就有了。初始值為0。
expectedModCount 是 ArrayList中的一個內(nèi)部類——Itr中的成員變量。
Iterator iterator = userNames.iterator();
以上代碼,即可得到一個 Itr類,該類實現(xiàn)了Iterator接口。
expectedModCount表示這個迭代器預(yù)期該集合被修改的次數(shù)。其值隨著Itr被創(chuàng)建而初始化。只有通過迭代器對集合進行操作,該值才會改變。
那么,接著我們看下userNames.remove(userName);方法里面做了什么事情,為什么會導(dǎo)致expectedModCount和modCount的值不一樣。
通過翻閱代碼,我們也可以發(fā)現(xiàn),remove方法核心邏輯如下:
private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
可以看到,它只修改了modCount,并沒有對expectedModCount做任何操作。
簡單畫一張圖描述下以上場景:
簡單總結(jié)一下,之所以會拋出CMException異常,是因為我們的代碼中使用了增強for循環(huán),而在增強for循環(huán)中,集合遍歷是通過iterator進行的,但是元素的add/remove卻是直接使用的集合類自己的方法。這就導(dǎo)致iterator在遍歷的時候,會發(fā)現(xiàn)有一個元素在自己不知不覺的情況下就被刪除/添加了,就會拋出一個異常,用來提示用戶,可能發(fā)生了并發(fā)修改!
所以,在使用Java的集合類的時候,如果發(fā)生CMException,優(yōu)先考慮fail-fast有關(guān)的情況,實際上這里并沒有真的發(fā)生并發(fā),只是Iterator使用了fail-fast的保護機制,只要他發(fā)現(xiàn)有某一次修改是未經(jīng)過自己進行的,那么就會拋出異常。
關(guān)于如何解決這種問題,我們在《為什么阿里巴巴禁止在 foreach 循環(huán)里進行元素的 remove/add 操作》中介紹過,這里不再贅述了。
fail-safe
為了避免觸發(fā)fail-fast機制,導(dǎo)致異常,我們可以使用Java中提供的一些采用了fail-safe機制的集合類。
這樣的集合容器在遍歷時不是直接在集合內(nèi)容上訪問的,而是先復(fù)制原有集合內(nèi)容,在拷貝的集合上進行遍歷。
java.util.concurrent包下的容器都是fail-safe的,可以在多線程下并發(fā)使用,并發(fā)修改。同時也可以在foreach中進行add/remove 。
我們拿CopyOnWriteArrayList這個fail-safe的集合類來簡單分析一下。
public static void main(String[] args) { List<String> userNames = new CopyOnWriteArrayList<String>() {{ add("Hollis"); add("hollis"); add("HollisChuang"); add("H"); }}; userNames.iterator(); for (String userName : userNames) { if (userName.equals("Hollis")) { userNames.remove(userName); } } System.out.println(userNames); }
以上代碼,使用CopyOnWriteArrayList代替了ArrayList,就不會發(fā)生異常。
fail-safe集合的所有對集合的修改都是先拷貝一份副本,然后在副本集合上進行的,并不是直接對原集合進行修改。并且這些修改方法,如add/remove都是通過加鎖來控制并發(fā)的。
所以,CopyOnWriteArrayList中的迭代器在迭代的過程中不需要做fail-fast的并發(fā)檢測。(因為fail-fast的主要目的就是識別并發(fā),然后通過異常的方式通知用戶)
但是,雖然基于拷貝內(nèi)容的優(yōu)點是避免了ConcurrentModificationException,但同樣地,迭代器并不能訪問到修改后的內(nèi)容。如以下代碼:
public static void main(String[] args) { List<String> userNames = new CopyOnWriteArrayList<String>() {{ add("Hollis"); add("hollis"); add("HollisChuang"); add("H"); }}; Iterator it = userNames.iterator(); for (String userName : userNames) { if (userName.equals("Hollis")) { userNames.remove(userName); } } System.out.println(userNames); while(it.hasNext()){ System.out.println(it.next()); } }
我們得到CopyOnWriteArrayList的Iterator之后,通過for循環(huán)直接刪除原數(shù)組中的值,最后在結(jié)尾處輸出Iterator,結(jié)果發(fā)現(xiàn)內(nèi)容如下:
[hollis, HollisChuang, H]
Hollis
hollis
HollisChuang
H
迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發(fā)生的修改迭代器是不知道的。
Copy-On-Write
在了解了CopyOnWriteArrayList之后,不知道大家會不會有這樣的疑問:他的add/remove等方法都已經(jīng)加鎖了,還要copy一份再修改干嘛?多此一舉?同樣是線程安全的集合,這玩意和Vector有啥區(qū)別呢?
Copy-On-Write簡稱COW,是一種用于程序設(shè)計中的優(yōu)化策略。其基本思路是,從一開始大家都在共享同一個內(nèi)容,當(dāng)某個人想要修改這個內(nèi)容的時候,才會真正把內(nèi)容Copy出去形成一個新的內(nèi)容然后再改,這是一種延時懶惰策略。
CopyOnWrite容器即寫時復(fù)制的容器。通俗的理解是當(dāng)我們往一個容器添加元素的時候,不直接往當(dāng)前容器添加,而是先將當(dāng)前容器進行Copy,復(fù)制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。
CopyOnWriteArrayList中add/remove等寫方法是需要加鎖的,目的是為了避免Copy出N個副本出來,導(dǎo)致并發(fā)寫。
但是,CopyOnWriteArrayList中的讀方法是沒有加鎖的。
public E get(int index) { return get(getArray(), index); }
這樣做的好處是我們可以對CopyOnWrite容器進行并發(fā)的讀,當(dāng)然,這里讀到的數(shù)據(jù)可能不是最新的。因為寫時復(fù)制的思想是通過延時更新的策略來實現(xiàn)數(shù)據(jù)的最終一致性的,并非強一致性。
**所以CopyOnWrite容器是一種讀寫分離的思想,讀和寫不同的容器。**而Vector在讀寫的時候使用同一個容器,讀寫互斥,同時只能做一件事兒。
以上所述是小編給大家介紹的Java fail-fast詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
SpringCloudAlibaba微服務(wù)調(diào)用組件OpenFeign的方法
Feign是Netflix開發(fā)的聲明式、模板化的HTTP客戶端,其靈感來自Retrofit、JAXRS-2.0以及WebSocket,Feign可幫助我們更加便捷、優(yōu)雅地調(diào)用HTTP API,這篇文章主要介紹了SpringCloudAlibaba微服務(wù)調(diào)用組件OpenFeign,需要的朋友可以參考下2024-07-07深入理解Java中的final關(guān)鍵字_動力節(jié)點Java學(xué)院整理
Java中的final關(guān)鍵字非常重要,它可以應(yīng)用于類、方法以及變量。這篇文章中我將帶你看看什么是final關(guān)鍵字以及使用final的好處,具體內(nèi)容詳情通過本文學(xué)習(xí)吧2017-04-04Spring 事務(wù)事件監(jiān)控及實現(xiàn)原理解析
本文首先會使用實例進行講解Spring事務(wù)事件是如何使用的,然后會講解這種使用方式的實現(xiàn)原理。感興趣的朋友跟隨小編一起看看吧2018-09-09如何通過java實現(xiàn)highcharts導(dǎo)出圖片至excel
這篇文章主要介紹了如何通過java實現(xiàn)highcharts導(dǎo)出圖片至excel。文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,下面我們就來一起學(xué)習(xí)一下吧2019-06-06