欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一不小心就讓Java開(kāi)發(fā)踩坑的fail-fast是個(gè)什么鬼?(推薦)

 更新時(shí)間:2019年04月19日 14:15:59   作者:HollisChuang  
這篇文章主要介紹了Java fail-fast,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

我在《為什么阿里巴巴禁止在 foreach 循環(huán)里進(jìn)行元素的 remove/add 操作》一文中曾經(jīng)介紹過(guò)Java中的fail-fast機(jī)制,但是并沒(méi)有深入介紹,本文,就來(lái)深入介紹一下fail-fast。

什么是fail-fast

首先我們看下維基百科中關(guān)于fail-fast的解釋?zhuān)?br />

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è)計(jì)中,快速失效系統(tǒng)一種可以立即報(bào)告任何可能表明故障的情況的系統(tǒng)??焖偈到y(tǒng)通常設(shè)計(jì)用于停止正常操作,而不是試圖繼續(xù)可能存在缺陷的過(guò)程。這種設(shè)計(jì)通常會(huì)在操作中的多個(gè)點(diǎn)檢查系統(tǒng)的狀態(tài),因此可以及早檢測(cè)到任何故障??焖偈∧K的職責(zé)是檢測(cè)錯(cuò)誤,然后讓系統(tǒng)的下一個(gè)最高級(jí)別處理錯(cuò)誤。

其實(shí),這是一種理念,說(shuō)白了就是在做系統(tǒng)設(shè)計(jì)的時(shí)候先考慮異常情況,一旦發(fā)生異常,直接停止并上報(bào)。

舉一個(gè)最簡(jiǎn)單的fail-fast的例子:

public int divide(int divisor,int dividend){
 if(dividend == 0){
  throw new RuntimeException("dividend can't be null");
 }
 return divisor/dividend;
}

上面的代碼是一個(gè)對(duì)兩個(gè)整數(shù)做除法的方法,在divide方法中,我們對(duì)被除數(shù)做了個(gè)簡(jiǎn)單的檢查,如果其值為0,那么就直接拋出一個(gè)異常,并明確提示異常原因。這其實(shí)就是fail-fast理念的實(shí)際應(yīng)用。

這樣做的好處就是可以預(yù)先識(shí)別出一些錯(cuò)誤情況,一方面可以避免執(zhí)行復(fù)雜的其他代碼,另外一方面,這種異常情況被識(shí)別之后也可以針對(duì)性的做一些單獨(dú)處理。

怎么樣,現(xiàn)在你知道fail-fast了吧,其實(shí)他并不神秘,你日常的代碼中可能經(jīng)常會(huì)在使用的。

既然,fail-fast是一種比較好的機(jī)制,為什么文章標(biāo)題說(shuō)fail-fast會(huì)有坑呢?

原因是Java的集合類(lèi)中運(yùn)用了fail-fast機(jī)制進(jìn)行設(shè)計(jì),一旦使用不當(dāng),觸發(fā)fail-fast機(jī)制設(shè)計(jì)的代碼,就會(huì)發(fā)生非預(yù)期情況。

集合類(lèi)中的fail-fast

我們通常說(shuō)的Java中的fail-fast機(jī)制,默認(rèn)指的是Java集合的一種錯(cuò)誤檢測(cè)機(jī)制。當(dāng)多個(gè)線程對(duì)部分集合進(jìn)行結(jié)構(gòu)上的改變的操作時(shí),有可能會(huì)產(chǎn)生fail-fast機(jī)制,這個(gè)時(shí)候就會(huì)拋出ConcurrentModificationException(后文用CME代替)。

CMException,當(dāng)方法檢測(cè)到對(duì)象的并發(fā)修改,但不允許這種修改時(shí)就拋出該異常。

很多時(shí)候正是因?yàn)榇a中拋出了CMException,很多程序員就會(huì)很困惑,明明自己的代碼并沒(méi)有在多線程環(huán)境中執(zhí)行,為什么會(huì)拋出這種并發(fā)有關(guān)的異常呢?這種情況在什么情況下才會(huì)拋出呢?我們就來(lái)深入分析一下。

異常復(fù)現(xiàn)

在Java中, 如果在foreach 循環(huán)里對(duì)某些集合元素進(jìn)行元素的 remove/add 操作的時(shí)候,就會(huì)觸發(fā)fail-fast機(jī)制,進(jìn)而拋出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);

以上代碼,使用增強(qiáng)for循環(huán)遍歷元素,并嘗試刪除其中的Hollis字符串元素。運(yùn)行以上代碼,會(huì)拋出以下異常:

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)

同樣的,讀者可以嘗試下在增強(qiáng)for循環(huán)中使用add方法添加元素,結(jié)果也會(huì)同樣拋出該異常。

在深入原理之前,我們先嘗試把foreach進(jìn)行解語(yǔ)法糖,看一下foreach具體如何實(shí)現(xiàn)的。

我們使用jad工具,對(duì)編譯后的class進(jìn)行反編譯,得到以下代碼:

 

public static void main(String[] args) {
 // 使用ImmutableList初始化一個(gè)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其實(shí)是依賴(lài)了while循環(huán)和Iterator實(shí)現(xiàn)的。

異常原理

通過(guò)以上代碼的異常堆棧,我們可以跟蹤到真正拋出異常的代碼是:

java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)

該方法是在iterator.next()方法中調(diào)用的。我們看下該方法的實(shí)現(xiàn):

final void checkForComodification() {
 if (modCount != expectedModCount)
  throw new ConcurrentModificationException();
}

如上,在該方法中對(duì)modCount和expectedModCount進(jìn)行了比較,如果二者不想等,則拋出CMException。

那么,modCount和expectedModCount是什么?是什么原因?qū)е滤麄兊闹挡幌氲鹊哪兀?/p>

modCount是ArrayList中的一個(gè)成員變量。它表示該集合實(shí)際被修改的次數(shù)。

List<String> userNames = new ArrayList<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
}};

當(dāng)使用以上代碼初始化集合之后該變量就有了。初始值為0。

expectedModCount 是 ArrayList中的一個(gè)內(nèi)部類(lèi)——Itr中的成員變量。

Iterator iterator = userNames.iterator();

以上代碼,即可得到一個(gè) Itr類(lèi),該類(lèi)實(shí)現(xiàn)了Iterator接口。

expectedModCount表示這個(gè)迭代器預(yù)期該集合被修改的次數(shù)。其值隨著Itr被創(chuàng)建而初始化。只有通過(guò)迭代器對(duì)集合進(jìn)行操作,該值才會(huì)改變。

那么,接著我們看下userNames.remove(userName);方法里面做了什么事情,為什么會(huì)導(dǎo)致expectedModCount和modCount的值不一樣。

通過(guò)翻閱代碼,我們也可以發(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,并沒(méi)有對(duì)expectedModCount做任何操作。

簡(jiǎn)單畫(huà)一張圖描述下以上場(chǎng)景:

簡(jiǎn)單總結(jié)一下,之所以會(huì)拋出CMException異常,是因?yàn)槲覀兊拇a中使用了增強(qiáng)for循環(huán),而在增強(qiáng)for循環(huán)中,集合遍歷是通過(guò)iterator進(jìn)行的,但是元素的add/remove卻是直接使用的集合類(lèi)自己的方法。這就導(dǎo)致iterator在遍歷的時(shí)候,會(huì)發(fā)現(xiàn)有一個(gè)元素在自己不知不覺(jué)的情況下就被刪除/添加了,就會(huì)拋出一個(gè)異常,用來(lái)提示用戶(hù),可能發(fā)生了并發(fā)修改!

所以,在使用Java的集合類(lèi)的時(shí)候,如果發(fā)生CMException,優(yōu)先考慮fail-fast有關(guān)的情況,實(shí)際上這里并沒(méi)有真的發(fā)生并發(fā),只是Iterator使用了fail-fast的保護(hù)機(jī)制,只要他發(fā)現(xiàn)有某一次修改是未經(jīng)過(guò)自己進(jìn)行的,那么就會(huì)拋出異常。

關(guān)于如何解決這種問(wèn)題,我們?cè)凇稙槭裁窗⒗锇桶徒乖?foreach 循環(huán)里進(jìn)行元素的 remove/add 操作》中介紹過(guò),這里不再贅述了。

fail-safe

為了避免觸發(fā)fail-fast機(jī)制,導(dǎo)致異常,我們可以使用Java中提供的一些采用了fail-safe機(jī)制的集合類(lèi)。

這樣的集合容器在遍歷時(shí)不是直接在集合內(nèi)容上訪問(wèn)的,而是先復(fù)制原有集合內(nèi)容,在拷貝的集合上進(jìn)行遍歷。

java.util.concurrent包下的容器都是fail-safe的,可以在多線程下并發(fā)使用,并發(fā)修改。同時(shí)也可以在foreach中進(jìn)行add/remove 。

我們拿CopyOnWriteArrayList這個(gè)fail-safe的集合類(lèi)來(lái)簡(jiǎn)單分析一下。

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,就不會(huì)發(fā)生異常。

fail-safe集合的所有對(duì)集合的修改都是先拷貝一份副本,然后在副本集合上進(jìn)行的,并不是直接對(duì)原集合進(jìn)行修改。并且這些修改方法,如add/remove都是通過(guò)加鎖來(lái)控制并發(fā)的。

所以,CopyOnWriteArrayList中的迭代器在迭代的過(guò)程中不需要做fail-fast的并發(fā)檢測(cè)。(因?yàn)閒ail-fast的主要目的就是識(shí)別并發(fā),然后通過(guò)異常的方式通知用戶(hù))

但是,雖然基于拷貝內(nèi)容的優(yōu)點(diǎn)是避免了ConcurrentModificationException,但同樣地,迭代器并不能訪問(wèn)到修改后的內(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之后,通過(guò)for循環(huán)直接刪除原數(shù)組中的值,最后在結(jié)尾處輸出Iterator,結(jié)果發(fā)現(xiàn)內(nèi)容如下:

[hollis, HollisChuang, H]
Hollis
hollis
HollisChuang
H

 迭代器遍歷的是開(kāi)始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發(fā)生的修改迭代器是不知道的。

Copy-On-Write

在了解了CopyOnWriteArrayList之后,不知道大家會(huì)不會(huì)有這樣的疑問(wèn):他的add/remove等方法都已經(jīng)加鎖了,還要copy一份再修改干嘛?多此一舉?同樣是線程安全的集合,這玩意和Vector有啥區(qū)別呢?

Copy-On-Write簡(jiǎn)稱(chēng)COW,是一種用于程序設(shè)計(jì)中的優(yōu)化策略。其基本思路是,從一開(kāi)始大家都在共享同一個(gè)內(nèi)容,當(dāng)某個(gè)人想要修改這個(gè)內(nèi)容的時(shí)候,才會(huì)真正把內(nèi)容Copy出去形成一個(gè)新的內(nèi)容然后再改,這是一種延時(shí)懶惰策略。

CopyOnWrite容器即寫(xiě)時(shí)復(fù)制的容器。通俗的理解是當(dāng)我們往一個(gè)容器添加元素的時(shí)候,不直接往當(dāng)前容器添加,而是先將當(dāng)前容器進(jìn)行Copy,復(fù)制出一個(gè)新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。

CopyOnWriteArrayList中add/remove等寫(xiě)方法是需要加鎖的,目的是為了避免Copy出N個(gè)副本出來(lái),導(dǎo)致并發(fā)寫(xiě)。

但是,CopyOnWriteArrayList中的讀方法是沒(méi)有加鎖的。

public E get(int index) {
  return get(getArray(), index);
}

這樣做的好處是我們可以對(duì)CopyOnWrite容器進(jìn)行并發(fā)的讀,當(dāng)然,這里讀到的數(shù)據(jù)可能不是最新的。因?yàn)閷?xiě)時(shí)復(fù)制的思想是通過(guò)延時(shí)更新的策略來(lái)實(shí)現(xiàn)數(shù)據(jù)的最終一致性的,并非強(qiáng)一致性。

**所以CopyOnWrite容器是一種讀寫(xiě)分離的思想,讀和寫(xiě)不同的容器。**而Vector在讀寫(xiě)的時(shí)候使用同一個(gè)容器,讀寫(xiě)互斥,同時(shí)只能做一件事兒。

以上所述是小編給大家介紹的Java fail-fast詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!

相關(guān)文章

最新評(píng)論