Java ConcurrentModificationException及fail-fast機(jī)制簡(jiǎn)析

前言

最近還是很忙,寫一些相對(duì)容易的大家都知道的知識(shí)點(diǎn)吧。

ConcurrentModificationException(下文簡(jiǎn)稱CME)呛凶,即并發(fā)修改異常,是Java集合操作中常見的一種異常行贪。本文通過示例及JDK源碼分析產(chǎn)生CME的內(nèi)部機(jī)制漾稀,并提出解決方法。

CME的產(chǎn)生

java.util包下很多集合的操作都可能會(huì)拋出CME建瘫,這里就以ArrayList為例崭捍。下面的程序產(chǎn)生包含10個(gè)元素的ArrayList,遍歷它啰脚,并在遍歷過程中隨機(jī)刪掉其中一個(gè)元素殷蛇。

public class CMEExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            list.add(String.valueOf(i));
        }
        String random = String.valueOf(new Random().nextInt(10) + 1);
        System.out.println(random);
        for (String s : list) {
            if (s.equals(random)) {
                list.remove(s);
            }
        }
    }
}

多次運(yùn)行以上程序,發(fā)現(xiàn)大多數(shù)情況均會(huì)拋出CME橄浓,如以下輸出:

6
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 me.lmagics.CMEExample.main(CMEExample.java:19)

但是當(dāng)要?jiǎng)h除元素"9"時(shí)粒梦,就不會(huì)拋出異常,執(zhí)行成功荸实。為什么會(huì)有兩種不同的情況匀们?下面就通過JDK源碼來(lái)分析。

CME原因分析(fail-fast機(jī)制)

CMEExample類中使用foreach循環(huán)來(lái)遍歷List准给,也就相當(dāng)于采用了迭代器昼蛀。ArrayList的迭代器實(shí)現(xiàn)位于私有內(nèi)部類Itr中宴猾,其源碼如下。

    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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

查看報(bào)錯(cuò)信息叼旋,CME是在調(diào)用Itr.next()方法時(shí)仇哆,從checkForComodification()方法拋出的,原因是modCount與expectedModCount的值不相等夫植。由上面的代碼可知讹剔,expectedModCount在Itr初始化時(shí)就賦值,其值等于modCount详民,所以modCount的值一定發(fā)生了變化延欠。

modCount字段定義在ArrayList的父類AbstractList中。

protected transient int modCount = 0;

根據(jù)JavaDoc的解釋沈跨,modCount記錄了一個(gè)列表發(fā)生結(jié)構(gòu)化修改(structurally modified由捎,如改變大小或打亂順序)的次數(shù),類似于版本號(hào)饿凛。

下圖展示了modCount字段會(huì)被ArrayList類中的哪些方法修改狞玛。注意添加元素的add()與addAll()方法并沒有出現(xiàn),但它們最終調(diào)用了ensureExplicitCapacity()方法涧窒,故也會(huì)修改modCount心肪。

示例代碼中調(diào)用remove(Object)方法后,嵌套的fastRemove()方法會(huì)增加modCount的值纠吴,變成11硬鞍,而expectedModCount的值仍為10,就拋出了CME戴已。

換句話說固该,ArrayList.remove()方法破壞了迭代器內(nèi)維護(hù)的集合修改狀態(tài)。如果在迭代過程中進(jìn)行了任何使modCount改變的操作(不管是單線程還是多線程的環(huán)境下)糖儡,為了防止繼續(xù)迭代下去發(fā)生不可預(yù)見的狀況蹬音,就會(huì)立即失敗并拋出CME,這就是所謂的快速失斝萃妗(fail-fast)機(jī)制著淆。

那么為什么刪除元素"9"就沒問題?這與迭代器的hasNext()方法有關(guān)拴疤。迭代器內(nèi)維護(hù)了指示當(dāng)前元素的游標(biāo)cursor永部,當(dāng)cursor與列表的大小size相同時(shí),表示沒有下一個(gè)元素呐矾,迭代過程結(jié)束苔埋。刪除掉元素“9”之后,cursor與size的值都是9蜒犯,所以會(huì)退出迭代组橄,next()方法根本不會(huì)執(zhí)行荞膘,只是一種巧合而已。

CME的解決方法

就示例中的刪除操作而言玉工,正確的方式是不調(diào)用容器的remove()方法羽资,而是調(diào)用迭代器本身的remove()方法,即:

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String s = iterator.next();
            if (s.equals(random)) {
                iterator.remove();
            }
        }

我們可以參考一下Itr.remove()方法的源碼遵班,它除了調(diào)用ArrayList.remove()方法之外屠升,還做了兩件額外的事:

  • 將游標(biāo)cursor重置回上一個(gè)讀取的元素下標(biāo)。
  • 將expectedModCount重新賦值成當(dāng)前的modCount狭郑。

這樣在刪除元素的同時(shí)腹暖,也維護(hù)了游標(biāo)位置和修改狀態(tài),因此能夠安全地繼續(xù)迭代翰萨。如果需要迭代時(shí)添加元素脏答,那么可以利用ArrayList提供的另一種迭代器ListIterator,它的功能更加豐富一點(diǎn)亩鬼,并且機(jī)制幾乎相同殖告,不再贅述。

多線程環(huán)境

文章開頭的例子是在單線程環(huán)境演示的辛孵,但從CME的命名“并發(fā)修改異炒园梗”來(lái)看赡磅,它似乎更偏向于多線程的情況魄缚。由于ArrayList本身就是線程不安全的,因此我們采用線程安全的列表Vector再做一次實(shí)驗(yàn)焚廊,以排除干擾冶匹。

public class CMEExample {
    public static void main(String[] args) {
        Vector<String> vector = new Vector<>();
        for (int i = 1; i <= 20; i++) {
            vector.add(String.valueOf(i));
        }
        String random = String.valueOf(new Random().nextInt(20) + 1);
        System.out.println("Random: " + random);

        Thread threadA = new Thread(() -> {
            ListIterator<String> iterator = vector.listIterator();
            while (iterator.hasNext()) {
                String s = iterator.next();
                System.out.println("Thread-A: " + s);
                if (s.equals(random)) {
                    iterator.add(s);
                }
            }
        }, "Thread-A");

        Thread threadB = new Thread(() -> {
            ListIterator<String> iterator = vector.listIterator();
            while (iterator.hasNext()) {
                System.out.println("Thread-B: " + iterator.next());
            }
        }, "Thread-B");

        threadA.start();
        threadB.start();
    }
}

這個(gè)示例創(chuàng)建了有20個(gè)元素的Vector與兩個(gè)線程。線程A遍歷該Vector咆瘟,并從中隨機(jī)選擇一個(gè)元素嚼隘,使用安全的ListIterator.add()方法再插入一個(gè)相同的元素;線程B則只做遍歷袒餐。其輸出如下:

Random: 16
Thread-A: 1
Thread-A: 2
Thread-A: 3
Thread-B: 1
Thread-B: 2
Thread-B: 3
Thread-B: 4
Thread-B: 5
Thread-A: 4
Thread-A: 5
Thread-A: 6
Thread-A: 7
Thread-B: 6
Thread-A: 8
Thread-A: 9
Thread-A: 10
Thread-A: 11
Thread-A: 12
Thread-A: 13
Thread-A: 14
Thread-A: 15
Thread-A: 16
Thread-B: 7
Thread-A: 17
Thread-A: 18
Thread-A: 19
Thread-A: 20
Exception in thread "Thread-B" java.util.ConcurrentModificationException
    at java.util.Vector$Itr.checkForComodification(Vector.java:1210)
    at java.util.Vector$Itr.next(Vector.java:1163)
    at me.lmagics.CMEExample.lambda$main$1(CMEExample.java:37)
    at java.lang.Thread.run(Thread.java:748)

可見飞蛹,CME與容器本身線程安全與否并沒有關(guān)系。在這種情況下灸眼,當(dāng)線程A添加元素之后卧檐,線程A中迭代器的expectedModCount會(huì)隨著modCount更新,但線程B中迭代器的expectedModCount并不會(huì)更新焰宣,進(jìn)而拋出CME霉囚。

要解決多線程CME問題,可以考慮對(duì)迭代過程加鎖匕积,確保多個(gè)線程不會(huì)同時(shí)遍歷列表盈罐,即:

        Thread threadB = new Thread(() -> {
            ListIterator<String> iterator = vector.listIterator();
            synchronized (vector) {
                while (iterator.hasNext()) {
                    System.out.println("Thread-B: " + iterator.next());
                }
            }
        }, "Thread-B");

我們還可以使用并發(fā)容器CopyOnWriteArrayList來(lái)替代普通的ArrayList與Vector榜跌。

CopyOnWriteArrayList(以及其他j.u.c包中的集合)是與快速失敗相反的安全失敗(fail-safe)機(jī)制的典型應(yīng)用盅粪。它會(huì)在寫操作時(shí)將集合復(fù)制一份副本钓葫,在副本上寫數(shù)據(jù),即COW(copy on write)湾揽,寫完成之后再將正本的引用指向副本瓤逼,而讀操作直接在正本上進(jìn)行。這種讀寫分離的方法使得它不會(huì)拋出CME库物,之后有時(shí)間的話霸旗,也會(huì)詳細(xì)地閱讀它的源碼。

fail-safe的容器比起fail-fast固然安全了很多戚揭,但是由于每次寫都要復(fù)制诱告,時(shí)間和空間的開銷都更高,因此只適合讀多寫少的情景民晒。在寫入時(shí)精居,為了盡量保證效率,也應(yīng)盡量做批量插入或刪除潜必,而不是單條操作靴姿。并且它的正本和副本有可能不同步,因此無(wú)法保證讀取的是最新數(shù)據(jù)磁滚,這也是CAP理論中一致性(consistency)與可用性(availability)矛盾的體現(xiàn)佛吓。

The End

晚安晚安(但是凌晨有全鏈路壓測(cè),希望窩的流式計(jì)算任務(wù)們能挺過去垂攘,bless

順便维雇,今晚終于下定決心升級(jí)Catalina了,為毛下載這么慢呢……

最后與Mojave合影留念(

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末晒他,一起剝皮案震驚了整個(gè)濱河市吱型,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌陨仅,老刑警劉巖津滞,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異灼伤,居然都是意外死亡触徐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門饺蔑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)锌介,“玉大人,你說我怎么就攤上這事】谆觯” “怎么了隆敢?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)崔慧。 經(jīng)常有香客問我拂蝎,道長(zhǎng),這世上最難降的妖魔是什么惶室? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任温自,我火速辦了婚禮,結(jié)果婚禮上皇钞,老公的妹妹穿的比我還像新娘悼泌。我一直安慰自己,他們只是感情好夹界,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布馆里。 她就那樣靜靜地躺著,像睡著了一般可柿。 火紅的嫁衣襯著肌膚如雪鸠踪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天复斥,我揣著相機(jī)與錄音营密,去河邊找鬼。 笑死目锭,一個(gè)胖子當(dāng)著我的面吹牛评汰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侣集,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼键俱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼兰绣!你這毒婦竟也來(lái)了世分?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤缀辩,失蹤者是張志新(化名)和其女友劉穎臭埋,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臀玄,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瓢阴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了健无。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荣恐。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叠穆,到底是詐尸還是另有隱情少漆,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布硼被,位于F島的核電站示损,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏嚷硫。R本人自食惡果不足惜检访,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仔掸。 院中可真熱鬧脆贵,春花似錦、人聲如沸起暮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鞋怀。三九已至双泪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間密似,已是汗流浹背焙矛。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留残腌,地道東北人村斟。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像抛猫,于是被迫代替她去往敵國(guó)和親蟆盹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355