從源碼分析非線程安全集合類的不安全迭代器

非線程安全集合類(這里的集合指容器Collection翻伺,非Set)的迭代器結(jié)合了及時(shí)失敗機(jī)制,但仍然是不安全的沮焕。這種不安全表現(xiàn)在許多方面:

  1. 并發(fā)修改“通扯至耄”導(dǎo)致及時(shí)失敗
  2. 單線程修改也可能導(dǎo)致及時(shí)失敗的“誤報(bào)”
  3. 迭代器會(huì)“丟失”某些并發(fā)修改行為,讓及時(shí)失敗失效

如果不了解其不安全之處就隨意使用峦树,就像給程序埋下了地雷辣辫,隨時(shí)可能引爆旦事,卻不可預(yù)知。
ArrayList是一個(gè)常用的非線程安全集合急灭,下面以基于ArrayList講解幾種代表情況姐浮。

及時(shí)失敗

及時(shí)失敗也叫快速失敗,fast-fail葬馋。
“及時(shí)失敗”的迭代器并不是一種完備的處理機(jī)制卖鲤,而只是“善意地”捕獲并發(fā)錯(cuò)誤,因此只能作為并發(fā)問題的預(yù)警指示器畴嘶。它們采用的實(shí)現(xiàn)方式是蛋逾,將計(jì)數(shù)器的變化與容器關(guān)聯(lián)起來:如果在迭代期間計(jì)數(shù)器被修改,那么hasNext或next將拋出ConcurrentModificationException窗悯。然而区匣,這種檢查是在沒有同步的情況下進(jìn)行的,因此可能會(huì)看到失效的計(jì)數(shù)器蒋院,而迭代器可能并沒有意識到已經(jīng)發(fā)生了修改亏钩。這是一種設(shè)計(jì)上的權(quán)衡,從而降低并發(fā)修改操作的檢測代碼對程序性能帶來的影響欺旧。

然而姑丑,及時(shí)失敗機(jī)制十分簡潔(簡單&清晰),同時(shí)對集合的性能影響十分小切端,所以大部分非線程安全的集合類仍然使用這種機(jī)制來進(jìn)行“善意”的提醒彻坛。

幾種非線程安全的代表情況

并發(fā)修改“通城晏洌”導(dǎo)致及時(shí)失敗

“通程ぴ妫”是因?yàn)榧皶r(shí)失敗的“善意”性質(zhì),它很多時(shí)候會(huì)給我們提醒钙蒙,但有時(shí)候也不會(huì)給出提醒茵瀑,有時(shí)候甚至給出某種意義上的錯(cuò)誤提醒。這一小節(jié)針對正常的情況躬厌,這是我們考察一個(gè)機(jī)制是否值得“采納并完善”的根本屬性马昨。

構(gòu)造下列程序:

…
private Collection users = new ArrayList(); // 所以應(yīng)使用CopyOnWriteArrayList
…
users.add(new User("張三",28));
users.add(new User("李四",25));
users.add(new User("王五",31));
…
public void run() {
    Iterator itrUsers = users.iterator();
    while(itrUsers.hasNext()){
        System.out.println("aaaa");
        User user = (User)itrUsers.next();
        if(“張三”.equals(user.getName())){ // 在迭代過程中修改集合
            itrUsers.remove();
        } else { // 正常輸出
            System.out.println(user);
        }
    }
}
…

忽略細(xì)節(jié),假設(shè)有多個(gè)線程在同時(shí)執(zhí)行run方法扛施,操作users集合鸿捧。這時(shí),“通掣碓”會(huì)導(dǎo)致及時(shí)失敗匙奴。這里的異常可能從next或remove方法中拋出(當(dāng)然這里是從next妄荔,因?yàn)閚ext先執(zhí)行):

private class Itr implements Iterator<E> {
…
    public boolean hasNext() {
        return cursor != size;
    }
    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() { // 迭代器的remove方法
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet); // 集合的remove方法
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
…
}

實(shí)際檢查并拋出異常的是checkForComodification方法:

private class Itr implements Iterator<E> {
…
int expectedModCount = modCount;
    …
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
    …
}

modCount是當(dāng)前集合的版本號泼菌,每次修改(增刪改)集合都會(huì)加 1谍肤;expectedModCount是當(dāng)前迭代器的版本號,在迭代器實(shí)例化時(shí)初始化為modCount哗伯,只有remove方法正常執(zhí)行(不拋出異常)才可以修改這個(gè)值荒揣,與modCount保持同步。

因此焊刹,如果在線程A正常迭代的過程中系任,線程B修改了users集合,modCount就會(huì)發(fā)生變化伴澄,這時(shí)赋除,線程B的expectedModCount能夠與modCount保持同步,線程A的expectedModCount卻發(fā)現(xiàn)自己與modCount不再同步非凌,從而拋出ConcurrentModificationException異常举农。

扯遠(yuǎn)些:
對于線程安全的集合類而言,我們不希望任何失敗敞嗡。但對于非線程安全的類颁糟,有人認(rèn)為“應(yīng)該在假設(shè)線程安全的情況下使用”,所以及時(shí)失敗機(jī)制完全沒有必要喉悴;有人認(rèn)為“集合類的狀態(tài)太多(所有非線程安全域的狀態(tài)數(shù)量的乘積)棱貌,并發(fā)使用時(shí)應(yīng)該給出錯(cuò)誤提醒,否則很難排查并發(fā)問題”箕肃,所以及時(shí)失敗機(jī)制很有必要婚脱。這個(gè)問題見仁見智,個(gè)人支持后者觀點(diǎn)勺像。

所以障贸,這種及時(shí)失敗的檢查是不完備的。

單線程修改也可能導(dǎo)致及時(shí)失敗的“誤報(bào)”

多線程并發(fā)修改集合時(shí)吟宦,拋出ConcurrentModificationException異常作為及時(shí)失敗的提醒篮洁,往往是我們期望的結(jié)果。然而殃姓,如果在單線程遍歷迭代器的過程中修改了集合袁波,也會(huì)拋出ConcurrentModificationException異常,看起來發(fā)生了及時(shí)失敗蜗侈。這不是我們期望的結(jié)果篷牌,是一種及時(shí)失敗的誤報(bào)。

我們改用集合的remove方法移除user“張三”:

…
public void run() {
    …
        if(“張三”.equals(user.getName())){ // 在迭代過程中修改集合
            users.remove(user); // itrUsers.remove();
        } else { // 正常輸出
            System.out.println(user);
        }
    …
}
…

假設(shè)只有一個(gè)線程執(zhí)行run方法踏幻,在”張三”被刪除之后枷颊,下一次執(zhí)行next方法時(shí),仍舊會(huì)拋出ConcurrentModificationException異常,也就是導(dǎo)致了及時(shí)失敗偷卧。

這時(shí)因?yàn)榧系膔emove方法并沒有維護(hù)集合修改的狀態(tài)(如對modCount&expectedModCount組合的修改和檢查):

public class ArrayList<E> extends AbstractList<E>
…
    public boolean remove(Object o) { // 集合的remove方法
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
…
}

這也讓我們更容易理解及時(shí)失敗的本質(zhì)——依托于對集合修改狀態(tài)的維護(hù)豺瘤。這里的主要原因看起來是“集合的remove方法破壞了正常維護(hù)的集合修改狀態(tài)”,但對于使用者而言听诸,集合在單線程環(huán)境下卻拋出了ConcurrentModificationException異常坐求,這是由于及時(shí)失敗機(jī)制沒有區(qū)分單線程與多線程的情況,統(tǒng)一給出同樣的提醒(拋出ConcurrentModificationException異常)晌梨,因而是及時(shí)失敗的誤報(bào)桥嗤。

迭代器會(huì)“丟失”某些并發(fā)修改行為,讓及時(shí)失敗失效

除了誤報(bào)仔蝌,及時(shí)失敗之僅限于“善意”(有提醒就是“善意”的泛领,沒有也不是“惡意”的)還體現(xiàn)在其可能“丟失”某些并發(fā)修改行為。在這里敛惊,“丟失”意味著不提醒——某些線程并發(fā)修改了當(dāng)前集合渊鞋,但沒有拋出ConcurrentModificationException異常,及時(shí)失敗機(jī)制失效了瞧挤。

主動(dòng)避過及時(shí)失敗的檢查

利用hasNext方法提前結(jié)束線程锡宋,可以主動(dòng)避過及時(shí)失敗的檢查,從而導(dǎo)致修改行為的丟失:

private class Itr implements Iterator<E> {
…
    public boolean hasNext() {
        return cursor != size; // 思考:如果刪除了集合的倒數(shù)第二個(gè)元素特恬,會(huì)發(fā)生什么执俩?
    }
…
}

還是單線程的場景下,假設(shè)我們刪除了集合的倒數(shù)第二個(gè)元素癌刽。這時(shí)next方法導(dǎo)致cursor=oldSize-1役首,同時(shí)remove方法導(dǎo)致newSize=oldSize-1(oldSize是集合修改之前的size值,newSize集合修改之后的)显拜;所以hasNext方法會(huì)返回false衡奥,讓用戶誤以為集合迭代已經(jīng)結(jié)束(實(shí)際上還有最后一個(gè)元素),從而循環(huán)終止(在我們的程序里用hasNext判斷是否結(jié)束)讼油,無法拋出ConcurrentModificationException異常杰赛,及時(shí)失敗失效了呢簸。

推廣到多線程的情景是一樣的矮台,因?yàn)閟ize是共享的。

及時(shí)失敗的實(shí)現(xiàn)是非線程安全的

很容易忽略的一點(diǎn)是根时,上述集合修改狀態(tài)的維護(hù)本身就是在沒有同步的情況下進(jìn)行的瘦赫,因此可能看到更多(遠(yuǎn)比上述要多)失效的集合修改狀態(tài),使迭代器意識不到集合發(fā)生了修改蛤迎,這是一種競態(tài)條件(Race Condition)确虱。

假設(shè)線程A進(jìn)入迭代器的remove方法,線程B進(jìn)入迭代器的next方法替裆,現(xiàn)在線程A執(zhí)行集合的remove方法:

private class Itr implements Iterator<E> {
…
    public void remove() {
        …
            ArrayList.this.remove(lastRet);
        …
    }
…
}

首先校辩,假設(shè)沒有其他線程并發(fā)修改窘问,則兩個(gè)線程都可以通過checkForComodification()的檢查;然后線程A快速的執(zhí)行集合的remove方法宜咒;待線程A執(zhí)行完集合的remove方法惠赫,由于線程B之前已經(jīng)通過了檢查,現(xiàn)在就無法意識到“users集合在線程A中已經(jīng)發(fā)生了變化”故黑。另外儿咱,因?yàn)閹缀跬耆淮嬖谕酱胧琺odCount的修改也存在競態(tài)條件场晶,其他狀態(tài)也無法保證是否有效混埠。

總結(jié)

上面看到了非線程安全集合類的迭代器是不安全的,但在單線程的環(huán)境下诗轻,這些集合類在性能钳宪、維護(hù)難度等方面仍然具有不可替代的優(yōu)勢。那么該如何在兼具一定程度線程安全的前提下扳炬,更好的發(fā)揮內(nèi)建集合類的優(yōu)勢呢使套?總結(jié)起來無非兩點(diǎn):

  1. 使用非線程安全的集合時(shí)(實(shí)際上對于某些“線程安全”的集合類,其迭代器也是線程不安全的)鞠柄,迭代過程中需要用戶自覺維護(hù)侦高,不修改該集合。
  2. 應(yīng)盡可能明確線程安全的需求等級厌杜,做好一致性奉呛、活躍性、性能等方面的平衡夯尽,再針對性的使用相應(yīng)的集合類瞧壮。

參考:

  • 傳智播客_張孝祥_Java多線程與并發(fā)庫高級應(yīng)用視頻教程/19_傳智播客_張孝祥_java5同步集合類的應(yīng)用.avi

本文鏈接:源碼|從源碼分析非線程安全集合類的不安全迭代器
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載匙握,演繹或用于商業(yè)目的咆槽,但是必須保留本文的署名及鏈接。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末圈纺,一起剝皮案震驚了整個(gè)濱河市秦忿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛾娶,老刑警劉巖灯谣,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蛔琅,居然都是意外死亡胎许,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辜窑,“玉大人钩述,你說我怎么就攤上這事∧滤椋” “怎么了切距?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惨远。 經(jīng)常有香客問我谜悟,道長,這世上最難降的妖魔是什么北秽? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任葡幸,我火速辦了婚禮,結(jié)果婚禮上贺氓,老公的妹妹穿的比我還像新娘蔚叨。我一直安慰自己,他們只是感情好辙培,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布蔑水。 她就那樣靜靜地躺著,像睡著了一般扬蕊。 火紅的嫁衣襯著肌膚如雪搀别。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天尾抑,我揣著相機(jī)與錄音歇父,去河邊找鬼。 笑死再愈,一個(gè)胖子當(dāng)著我的面吹牛榜苫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播翎冲,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼垂睬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了抗悍?” 一聲冷哼從身側(cè)響起驹饺,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎檐春,沒想到半個(gè)月后逻淌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體么伯,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疟暖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片俐巴。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡骨望,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出欣舵,到底是詐尸還是另有隱情擎鸠,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布缘圈,位于F島的核電站劣光,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏糟把。R本人自食惡果不足惜绢涡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望遣疯。 院中可真熱鬧雄可,春花似錦、人聲如沸缠犀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辨液。三九已至虐急,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間滔迈,已是汗流浹背戏仓。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留亡鼠,地道東北人赏殃。 一個(gè)月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像间涵,于是被迫代替她去往敵國和親仁热。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Java-Review-Note——4.多線程 標(biāo)簽: JavaStudy PS:本來是分開三篇的勾哩,后來想想還是整...
    coder_pig閱讀 1,653評論 2 17
  • 1.Java集合框架是什么抗蠢?說出一些集合框架的優(yōu)點(diǎn)? 每種編程語言中都有集合思劳,最初的Java版本包含幾種集合類:V...
    獨(dú)念白閱讀 770評論 0 2
  • 1.Java集合框架是什么迅矛?說出一些集合框架的優(yōu)點(diǎn)? 每種編程語言中都有集合潜叛,最初的Java版本包含幾種集合類:V...
    joshul閱讀 373評論 0 2
  • 1.Java集合框架是什么秽褒?說出一些集合框架的優(yōu)點(diǎn)壶硅?每種編程語言中都有集合,最初的Java版本包含幾種集合類:Ve...
    yjaal閱讀 1,177評論 1 10
  • 標(biāo)簽(空格分隔): Java集合框架 問題思考 什么是集合框架销斟? 為什么用集合框架庐椒? 怎么用集合框架? 問題解決 ...
    outSiderYN閱讀 675評論 0 13