本文首發(fā)于公眾號(hào)污茵,關(guān)注文末公眾號(hào),閱讀體驗(yàn)更佳葬项。
這是個(gè)人第10篇原創(chuàng)文章
全文共計(jì)7362個(gè)字泞当,46張圖。分析的較為詳盡民珍,并進(jìn)行了相關(guān)知識(shí)點(diǎn)的擴(kuò)展襟士,所以篇幅較長(zhǎng),建議轉(zhuǎn)發(fā)朋友圈或者自己收藏起來嚷量,慢慢閱讀陋桂。
本文目錄
一.題是什么題?
二.阿里Java開發(fā)規(guī)范。
2.1 正例代碼蝶溶。
2.2 反例代碼嗜历。
三.層層揭秘,為什么發(fā)生異常了呢?
3.1 第一層:異常信息解讀抖所。
3.2 第二層:拋出異常的條件解讀秸脱。
3.3 第三層:什么是modCount?它是干啥的?什么時(shí)候發(fā)生變化?
3.4 第四層:什么是expectedModCount?它是干啥的?什么時(shí)候發(fā)生變化?
3.5 第五層:組裝線索,直達(dá)真相。
四.這題的坑在哪?
4.1 回頭再看部蛇。
4.2 還有一個(gè)騷操作。
五.線程安全版的ArrayList咐蝇。
六.總結(jié)一下涯鲁。
七.回答另外一個(gè)面試題。
八.擴(kuò)展閱讀有序。
7.1 fail-fast機(jī)制和safe-fast機(jī)制抹腿。
7.2 Java語法糖。
7.3 阿里Java開發(fā)手冊(cè)旭寿。
九.最后說一句警绩。
一.題是什么題?
我第一次遇到這個(gè)題的時(shí)候盅称,是在一個(gè)微信群里肩祥,阿里著名的"Java勸退師"小馬哥拋出了這樣的一個(gè)問題:
然后大家紛紛給出了自己的見解(注:刪除了部分聊天記錄):
后面在另外的群里聊天的時(shí)候(注:刪除了部分聊天記錄),我也拋出了這樣的問題:
總結(jié)一下圖片中的各種回答:
1.什么也不會(huì)發(fā)生,remove之后缩膝,list中的數(shù)據(jù)會(huì)被清空混狠。
2.remove的方法調(diào)用錯(cuò)誤,入?yún)?yīng)該是index(數(shù)組下標(biāo))疾层。
3.并發(fā)操作的時(shí)候會(huì)出現(xiàn)異常将饺。
4.會(huì)發(fā)生ConcurrentModifyException。
你的答案又是什么呢?
在這里予弧,我先不說正確的答案是什么刮吧,也先不評(píng)價(jià)這些回答是對(duì)是錯(cuò),我們一起去探索真相掖蛤,尋找答案杀捻。
二.阿里Java開發(fā)規(guī)范
有人看到題的第一眼(沒有認(rèn)真讀題),就想起了阿里java開發(fā)手冊(cè)(先入為主)坠七,里面是這樣說的:
正是因?yàn)榇蠖鄶?shù)人都知道并且讀過這個(gè)規(guī)范(畢竟是業(yè)界權(quán)威)水醋。所以呼聲最高的答案是【會(huì)發(fā)生ConcurrentModifyException】。因?yàn)樗麄冎腊⒗飆ava開發(fā)手冊(cè)里面是強(qiáng)制要求:
不要在foreach循環(huán)里面進(jìn)行元素的remove/add操作彪置。remove元素請(qǐng)使用Iterator方式拄踪,如果并發(fā)操作,需要對(duì)Iterator對(duì)象加鎖拳魁。
但是不能因?yàn)樗菣?quán)威惶桐,我們就全盤接受吧?
2.1 正例代碼
所以我們眼見為實(shí)潘懊,先把手冊(cè)里面提到的【正例代碼】跑一下姚糊,如下:
細(xì)心的讀者可能發(fā)現(xiàn)了:咦,這個(gè)代碼的22行為啥顏色不一樣呢授舟?
我?guī)湍憧纯础?/p>
替換之后的代碼是這樣的:
從上面我們可以得到一個(gè)結(jié)論.......
等等救恨,到這一步你就想得到結(jié)論了?你不對(duì)【一行代碼為什么就替換了七行代碼】好奇嗎释树?
看到真相的時(shí)候肠槽,有時(shí)候再往前一步就是本質(zhì)了。
源碼之下無秘密奢啥,我再送你一張圖秸仙,JDK1.8中Collection.removeIf的源碼:
好了,已經(jīng)到源碼級(jí)別了桩盲,從這里我們驗(yàn)證了寂纪,阿里java開發(fā)手冊(cè)里面的正例是對(duì)的,而且我還想給他加上一句:
如果你的JDK版本是1.8以上赌结,沒有并發(fā)訪問的情況下捞蛋,可以使用Collection.removeIf(Predicate<? super E> filter)方法。使代碼更加優(yōu)雅柬姚。
2.2 反例代碼
接下來我們看看【反例代碼】的運(yùn)行結(jié)果:
從執(zhí)行結(jié)果來看襟交,和我們預(yù)期的結(jié)果是一致∩丝浚看著沒有問題呀捣域?
但是你別忘了啼染,下面還有一句話啊:
我們執(zhí)行試一試:
什么情況?真的是"出乎意料"盎烂贰迹鹅!
把刪除元素的條件從【公眾號(hào)】修改為【why技術(shù)】就發(fā)生了異常:
java.util.ConcurrentModificationException
三.層層揭秘,為什么發(fā)生了異常呢?
我們現(xiàn)在明白為什么阿里強(qiáng)制要求不要在foreach循環(huán)里面進(jìn)行元素的remove/add操作贞言,因?yàn)闀?huì)發(fā)生異常了斜棚。
但是開發(fā)手冊(cè)里面并沒有告訴你,為什么會(huì)發(fā)生異常该窗。需要我們自己層層深入弟蚀,積極探索。
3.1 第一層:異常信息解讀
所以這一小節(jié)我們就一起探索酗失,為什么會(huì)發(fā)生異常义钉。我們?cè)俳馕鲆幌鲁绦虻倪\(yùn)行結(jié)果,如下:
正如上圖里面異常信息的體現(xiàn),異常是在代碼的第21行觸發(fā)的规肴。而代碼的第21行捶闸,是一個(gè)foreach循環(huán)。foreach循環(huán)是Java的語法糖拖刃,我們可以從編譯后的class文件中看出删壮,如下圖所示:
請(qǐng)注意圖中的第26行代碼:
list.remove(item) (這句話很關(guān)鍵!6夷怠Q氲)
很關(guān)鍵,很重要均函,后面會(huì)講到硬耍。
這也解釋了,異常信息里面的這一個(gè)問題:
好了边酒,到這一步,我們把異常信息都解讀完畢了狸窘。
3.2 第二層:拋出異常的條件解讀
我再看看真實(shí)拋出異常的那一個(gè)方法:
很簡(jiǎn)單墩朦,很清晰的四行代碼。拋出異常的條件是:
modCount !=expectedModCount
所以翻擒,我們需要解開的下兩層面紗就是下面兩大點(diǎn):
第一:什么是modCount氓涣?它是干啥的?什么時(shí)候發(fā)生變化陋气?
第二:什么是expectedModCount劳吠?它是干啥的?什么時(shí)候發(fā)生變化巩趁?
3.3 第三層:什么是modCount?它是干啥的痒玩?什么時(shí)候發(fā)生變化?
先來第一個(gè):什么是modCount?
modCount上的注釋很長(zhǎng),我只截取了最后一段蠢古。在這一段中奴曙,提到了兩個(gè)關(guān)鍵點(diǎn)。
1.modCount這個(gè)字段位于java.util.AbstractList抽象類中草讶。
2.modCount的注釋中提到了"fail-fast"機(jī)制洽糟。
3.如果子類希望提供"fail-fast"機(jī)制,需要在add(int,E)方法和remove(int)方法中對(duì)這個(gè)字段進(jìn)行處理堕战。
4.從第三點(diǎn)我們知道了坤溃,在提供了"fail-fast"機(jī)制的容器中(比如ArrayList),除了文中示例的remove(Obj)方法會(huì)導(dǎo)致ConcurrentModificationException異常嘱丢,add及其相關(guān)方法也會(huì)導(dǎo)致異常薪介。
知道了什么是modCount。那modCount是干啥的呢屿讽?
在提供了"fail-fast"機(jī)制的集合中昭灵,modCount的作用是記錄了該集合在使用過程中被修改的次數(shù)。
證據(jù)就在源碼里面伐谈,如下:
這是java.util.ArrayList#add(int, E)方法的源碼截圖:
這是java.util.ArrayList#remove(int)方法的源碼截圖:
注:這里不討論手動(dòng)設(shè)置為null是否對(duì)GC有幫助烂完,我個(gè)人認(rèn)為,在這里有這一行代碼并沒有壞處诵棵。在實(shí)際開發(fā)過程中抠蚣,一般不需要考慮到這點(diǎn)。
同時(shí)履澳,上面的源碼截圖也回答了這一層的最后一個(gè)問題:它什么時(shí)候被修改嘶窄?
拿ArrayList來說,當(dāng)調(diào)用add相關(guān)和remove相關(guān)方法時(shí)距贷,會(huì)觸發(fā)modCount++操作柄冲,從而被修改。
好了忠蝗,通過上面的分析现横,我們知道了什么是modCount和modCount是干啥的。準(zhǔn)備進(jìn)入第四層阁最。
3.4 第四層:什么是expectedModCount戒祠?它是干啥的?什么時(shí)候發(fā)生變化速种?
接下來:什么是expectedModCount姜盈?
expectedModCount是ArrayList中一個(gè)名叫Itr內(nèi)部類的成員變量。
第二問:expectedModCount它是干啥的:
它代表的含義是在這個(gè)迭代器中配阵,預(yù)期的修改次數(shù)
第三問:expectedModCount什么時(shí)候發(fā)生變化馏颂?
情況一:從上圖中也可以看出當(dāng)Itr初始化的時(shí)候示血,會(huì)對(duì)expectedModCount字段賦初始值,其值等于modCount饱亮。
情況二:如下圖所示矾芙,調(diào)用Itr的remove方法后會(huì)再次把modCount的值賦給expectedModCount。
換句話說就是:調(diào)用迭代器的remove會(huì)維護(hù)expectedModCount=modCount近上。(這句話很關(guān)鍵L尴堋!R嘉蕖)
好了分析到了這里葱绒,我們知道了下面這個(gè)六連擊:
1.什么是modCount?
2.modCount是干啥的?
3.modCount什么時(shí)候發(fā)生變化斗锭?
4.什么是expectedModCount地淀?
5.expectedModCount是干啥的?
6.expectedModCount什么時(shí)候發(fā)生變化岖是?
3.5 第五層:組裝線索帮毁,直達(dá)真相
為什么發(fā)生了異常呢?
如果說前四層是線索的話豺撑,真相其實(shí)已經(jīng)隱藏在線索里面了烈疚。我?guī)闶崂硪幌拢?/p>
【第一層:異常信息解讀】中說到:
【第二層:拋出異常的條件解讀】中說到:
【第三層:什么是modCount?它是干啥的?什么時(shí)候發(fā)生變化聪轿?】中說到:
【第四層:什么是expectedModCount爷肝?它是干啥的?什么時(shí)候發(fā)生變化陆错?】中說到:
為什么發(fā)生了異常呢灯抛?我想你大概已經(jīng)有了一個(gè)答案了,我再去Debug一下音瓷,為了方便演示对嚼,我們?nèi)サ粽Z法糖,程序修改如下:
并確認(rèn)一下這個(gè)循環(huán)體會(huì)執(zhí)行三次绳慎,如下:
第一次循環(huán)
第一次循環(huán)取出的【公眾號(hào)】纵竖,不滿足條件if("why技術(shù)".equals(item)),不會(huì)觸發(fā)list.remove(Obj)方法偷线。
第二次循環(huán)
如圖所示,第二次循環(huán)取到了“why技術(shù)”沽甥。滿足條件if("why技術(shù)".equals(item))声邦,會(huì)觸發(fā)list.remove(Obj)方法,如下所示:
第三次循環(huán)
總結(jié)一下在foreach循環(huán)里面進(jìn)行元素的remove/add操作拋出異常的真相:
因?yàn)閒oreach循環(huán)是Java的語法糖摆舟,經(jīng)過編譯后還原成了迭代器亥曹。
但是從經(jīng)過編譯后的代碼的第26行可以看出邓了,remove方法的調(diào)方是list,而不是迭代器媳瞪。
經(jīng)過前面的源碼分析我們知道骗炉,由于ArrayList的"fail-fast"機(jī)制,調(diào)用remove方法會(huì)觸發(fā)【modCount++】操作蛇受,對(duì)expectedModCount沒有任何操作句葵。只有調(diào)用迭代器的remove方法,才會(huì)維護(hù)expectedModCount=modCount兢仰。
所以調(diào)用了list的remove方法后乍丈,再調(diào)用Itr的next方法時(shí),導(dǎo)致了expectedModCount把将!=modCount轻专,拋出異常。
四.這題的坑在哪里察蹲?
前面講了阿里開發(fā)手冊(cè)请垛。講了在foreach循環(huán)里面進(jìn)行元素的remove/add為什么會(huì)發(fā)生異常。有了這些鋪墊之后洽议。
4.1 回頭再看
我們?cè)倩剡^頭來看小馬哥出的這個(gè)題:
我靠宗收,這乍一看,foreach循環(huán)里面調(diào)用list.remove(obj)绞铃。我們剛剛分析過镜雨,會(huì)拋出ConcurrentModificationException異常。
你要這樣答儿捧,你就進(jìn)了小馬哥的坑了荚坞。
這個(gè)題的坑在這三個(gè)點(diǎn)里面。小馬哥并沒有說這個(gè)list是ArrayList吧菲盾?如果你沒有認(rèn)真審題颓影,先入為主的默認(rèn)了這個(gè)list就是ArrayList。第一步就錯(cuò)了懒鉴。
這是真正的高手诡挂,借力打力。借阿里開發(fā)手冊(cè)的力临谱,讓你第一步就走錯(cuò)璃俗。
請(qǐng)看下面這張圖:
當(dāng)使用CopyOnWriteArrayList的時(shí)候,程序正常執(zhí)行悉默。
4.2 還有一個(gè)騷操作
既然我們知道為什么會(huì)拋出異常城豁,也知道怎么不拋出異常,List本來就是一個(gè)接口抄课,那我們是不是可以實(shí)現(xiàn)這個(gè)接口唱星,弄一個(gè)自定義的List呢雳旅?
比如下面的這個(gè)WhyTechnologyList,就是我自己的List间聊,貍貓換太子攒盈,這操作,夠"騷"啊哎榴。
只有掌握了原理型豁,我們想怎么玩就怎么玩。
五.線程安全版的ArrayList
CopyOnWriteArrayList是什么叹话?我們看一下源碼注釋上面是怎么說的:
相對(duì)于ArrayList而言偷遗,CopyOnWriteArrayList集合是線程安全的容器。在遍歷的時(shí)候驼壶,由于它操作是數(shù)組的"快照"氏豌,"快照"不會(huì)發(fā)生變化。所以它不需要額外加鎖热凹,也不會(huì)拋出ConcurrentModificationException異常泵喘。
我們主要看一下,示例程序中用到的三個(gè)方法般妙,add(E e)纪铺、next()、remove(Obj)
先看add(E e)方法:
我們看一下它的next()方法:
再看一下它的remove(Obj)方法:
next碟渺、remove都是操作的快照鲜锚,并沒有看到ArrayList里面的modCount和expectedModCount。所以它沒有拋出ConcurrentModificationException
之前看小馬哥說的這句話的時(shí)候還不太明白集合和一致性之間的關(guān)系(老問題苫拍,還是先入為主芜繁,一說到一致性首先想到的是緩存和數(shù)據(jù)庫之間的一致性)。
但是當(dāng)我閱讀源碼绒极,從add方法可以看出CopyOnWriteArrayList并不保證數(shù)據(jù)的實(shí)時(shí)一致性骏令。只能保證最終一致性。
同時(shí)我們從源碼中可以看出CopyOnWriteArrayList增刪改數(shù)據(jù)的時(shí)候需要搞一個(gè)"快照"垄提,這一點(diǎn)是比較耗內(nèi)存的榔袋,使用過程中需要注意。
六.總結(jié)一下
我們?cè)倩氐阶铋_始的地方铡俐,看看大家的回答:
1.什么也不會(huì)發(fā)生,remove之后凰兑,list中的數(shù)據(jù)會(huì)被清空。
2.remove的方法調(diào)用錯(cuò)誤审丘,入?yún)?yīng)該是index(數(shù)組下標(biāo))吏够。
3.并發(fā)操作的時(shí)候會(huì)出現(xiàn)異常。
4.會(huì)發(fā)生ConcurrentModifyException。
現(xiàn)在稿饰,你知道這些回答的問題在哪里了吧被啼?這一部分的總結(jié)也很簡(jiǎn)單幅狮,上一個(gè)對(duì)比圖就好了:
七.回答另外一個(gè)面試題
現(xiàn)在面試官經(jīng)常問的一個(gè)問題康二,你讀過源碼嗎亮靴?
咦皆看,巧了鼎兽。你看了這篇文章夜郁,就相當(dāng)于了讀了ArrayList和CopyOnWriteArrayList的部分源碼儒将。
那你就可以這樣回答啦:我之前看阿里Java開發(fā)手冊(cè)的時(shí)候看到一條規(guī)則是
不要在foreach循環(huán)里面進(jìn)行元素的remove/add操作沉噩。remove元素請(qǐng)使用Iterator方式捺宗,如果并發(fā)操作,需要對(duì)Iterator對(duì)象加鎖川蒙。
我對(duì)這條規(guī)則很感興趣蚜厉,所以我對(duì)其進(jìn)行了深入的研究,閱讀了
ArrayList和CopyOnWriteArrayList的部分源碼畜眨。
如果碰巧面試官也讀過這塊源碼昼牛,這個(gè)問題,你們可以相談甚歡康聂。
如果面試官?zèng)]有讀過這塊源碼贰健,你可以給他講的明明白白。
當(dāng)然恬汁,還有一個(gè)前提是:我希望你讀完這篇文章后伶椿,如果是第一次知道這個(gè)知識(shí)點(diǎn),那你可以自己實(shí)際操作一下氓侧。
看懂了是一回事脊另,自己再實(shí)際操作一下,是另外一回事甘苍。
八.擴(kuò)展閱讀
8.1 fail-fast和fail-safe機(jī)制
文中多次提到了"fail-fast"機(jī)制(快速失敗)尝蠕,與其對(duì)應(yīng)的還有"fail-safe"機(jī)制(失敗安全)。
這種機(jī)制是一種思想载庭,它不僅僅是體現(xiàn)在Java的集合中看彼。在我們常用的rpc框架Dubbo中,在集群容錯(cuò)時(shí)也有相關(guān)的實(shí)現(xiàn)囚聚。
Dubbo 主要提供了這樣幾種容錯(cuò)方式:
Failover Cluster - 失敗自動(dòng)切換
Failfast Cluster - 快速失敗
Failsafe Cluster - 失敗安全
Failback Cluster - 失敗自動(dòng)恢復(fù)
Forking Cluster - 并行調(diào)用多個(gè)服務(wù)提供者
如果對(duì)這兩種機(jī)制感興趣的朋友可以查閱相關(guān)資料靖榕,進(jìn)行了解。如果想要了解Dubbo的集群容錯(cuò)機(jī)制顽铸,可以看官方文檔茁计,地址如下:
http://dubbo.apache.org/zh-cn/docs/source_code_guide/cluster.html
8.2 Java語法糖
文中說到foreach循環(huán)的時(shí)候提到了Java的語法糖。如果對(duì)這一塊有興趣的讀者谓松,可以在網(wǎng)上查閱相關(guān)資料星压,也可以看看《深入理解Java虛擬機(jī)》的第10.3節(jié)践剂,有專門的介紹。
書中說到:
總而言之娜膘,語法糖可以看做是編譯器實(shí)現(xiàn)的一些“小把戲”逊脯,這些“小把戲”可能會(huì)使得效率“大提升”,但我們也應(yīng)該去了解這些“小把戲”背后的真實(shí)世界竣贪,那樣才能利用好它們军洼,而不是被它們所迷惑。
關(guān)注公眾號(hào)并回復(fù)關(guān)鍵字【Java】演怎。即可獲得此書的電子版匕争。
8.3 阿里Java開發(fā)手冊(cè)
阿里的孤盡大佬作為主要作者寫的這本《阿里Java開發(fā)手冊(cè)》,可以說是嘔心瀝血推出的業(yè)界權(quán)威爷耀,非常值得閱讀甘桑。讀完此書,你不僅能夠獲得很多干貨歹叮,甚至你還能讀出一點(diǎn)技術(shù)情懷在里面扇住。
對(duì)于技術(shù)情懷,孤盡大佬是這樣的說的:
熱愛盗胀、思考艘蹋、卓越。熱愛是一種源動(dòng)力票灰,而思考是一個(gè)過程女阀,而卓越是一個(gè)結(jié)果。如果給這三個(gè)詞加一個(gè)定語屑迂,使技術(shù)情懷更加立體浸策、清晰地被解讀,那就是奉獻(xiàn)式的熱愛惹盼,主動(dòng)式的思考庸汗,極致式的卓越。
關(guān)注公眾號(hào)并回復(fù)關(guān)鍵字【Java】手报。即可獲得此書的電子版蚯舱。
九.最后說一點(diǎn)
這篇文章寫之前我一直在糾結(jié),因?yàn)楦杏X這個(gè)知識(shí)點(diǎn)其實(shí)我已經(jīng)掌握了掩蛤,那我還有寫的必要嗎枉昏?我在寫的這個(gè)過程中還能收獲一些東西嗎?
但是在寫的過程中揍鸟,我翻閱了大量的源碼兄裂,雖然之前已經(jīng)看過,但是沒有這樣一行一行仔細(xì)的去分析。之前只是一個(gè)大概的模糊的影像晰奖,現(xiàn)在具象化清晰了起來谈撒,在這個(gè)過程中,我還是學(xué)到了很多很多匾南。
其實(shí)想到寫什么內(nèi)容并不難港华,難的是你對(duì)內(nèi)容的把控。關(guān)于技術(shù)性的語言午衰,我是反復(fù)推敲,查閱大量文章來進(jìn)行證偽冒萄,總之慎言慎言再慎言臊岸,畢竟做技術(shù),我認(rèn)為是一件非常嚴(yán)謹(jǐn)?shù)氖虑樽鹆鳎页3O胂笞约壕褪窃诠蕦m修文物的工匠帅戒,在工匠精神的認(rèn)知上,目前我可能和他們還差的有點(diǎn)遠(yuǎn)崖技,但是我時(shí)常以工匠精神要求自己逻住。就像我之前表達(dá)的:對(duì)于技術(shù)文章(因?yàn)槲遗紶栆矔?huì)荒腔走板的聊一聊生活,寫一寫書評(píng)迎献,影評(píng))瞎访,我盡量保證周推,全力保證質(zhì)量吁恍。
文中提到的兩本書《深入理解Java虛擬機(jī)》和《阿里Java開發(fā)手冊(cè)》是兩本非常優(yōu)秀扒秸,值得反復(fù)閱讀的工具書,可以關(guān)注我后冀瓦,在后臺(tái)發(fā)送java伴奥,即可獲得電子書。
才疏學(xué)淺翼闽,難免會(huì)有紕漏拾徙,如果你發(fā)現(xiàn)了錯(cuò)誤的地方,還請(qǐng)你留言給我指出來感局,我對(duì)其加以修改尼啡。
如果你覺得文章還不錯(cuò),你的點(diǎn)贊询微、留言玄叠、轉(zhuǎn)發(fā)、分享拓提、贊賞就是對(duì)我最大的鼓勵(lì)读恃。
另外,如果小馬哥本尊能讀到這個(gè)文章,讀到這段話寺惫,我想在這里表達(dá)對(duì)他的敬意疹吃。同時(shí)也想催更一下:小馬哥,每日一問好久沒更新啦西雀,非常懷戀那種被"坑"的明明白白的感覺萨驶!
以上。
謝謝您的閱讀艇肴,感謝您的關(guān)注腔呜。
歡迎關(guān)注公眾號(hào)【why技術(shù)】。在這里我會(huì)分享一些技術(shù)相關(guān)的東西再悼,主攻java方向核畴,用匠心敲代碼,對(duì)每一行代碼負(fù)責(zé)冲九。偶爾也會(huì)荒腔走板的聊一聊生活谤草,寫一寫書評(píng),影評(píng)莺奸。愿你我共同進(jìn)步丑孩。