本文首發於公衆號,關注文末公衆號,閱讀體驗更佳。html
這是我的第10篇原創文章java
全文共計7362個字,46張圖。分析的較爲詳盡,並進行了相關知識點的擴展,因此篇幅較長,建議轉發朋友圈或者本身收藏起來,慢慢閱讀。面試
一.題是什麼題?數據庫
二.阿里Java開發規範。apache
2.1 正例代碼。 2.2 反例代碼。
三.層層揭祕,爲何發生異常了呢?segmentfault
3.1 第一層:異常信息解讀。 3.2 第二層:拋出異常的條件解讀。 3.3 第三層:什麼是modCount?它是幹啥的?何時發生變化? 3.4 第四層:什麼是expectedModCount?它是幹啥的?何時發生變化? 3.5 第五層:組裝線索,直達真相。
四.這題的坑在哪?數組
4.1 回頭再看。 4.2 還有一個騷操做。
五.線程安全版的ArrayList。緩存
六.總結一下。安全
七.回答另一個面試題。微信
八.擴展閱讀。
7.1 fail-fast機制和safe-fast機制。 7.2 Java語法糖。 7.3 阿里Java開發手冊。
九.最後說一句。
我第一次遇到這個題的時候,是在一個微信羣裏,阿里著名的"Java勸退師"小馬哥拋出了這樣的一個問題:
而後你們紛紛給出了本身的看法(注:刪除了部分聊天記錄):
後面在另外的羣裏聊天的時候(注:刪除了部分聊天記錄),我也拋出了這樣的問題:
總結一下圖片中的各類回答:
1.什麼也不會發生,remove以後,list中的數據會被清空。
2.remove的方法調用錯誤,入參應該是index(數組下標)。
3.併發操做的時候會出現異常。
4.會發生ConcurrentModifyException。
你的答案又是什麼呢?
在這裏,我先不說正確的答案是什麼,也先不評價這些回答是對是錯,咱們一塊兒去探索真相,尋找答案。
有人看到題的第一眼(沒有認真讀題),就想起了阿里java開發手冊(先入爲主),裏面是這樣說的:
正是由於大多數人都知道而且讀過這個規範(畢竟是業界權威)。因此呼聲最高的答案是【會發生ConcurrentModifyException】。由於他們知道阿里java開發手冊裏面是強制要求:
不要在foreach循環裏面進行元素的remove/add操做。remove元素請使用Iterator方式,若是併發操做,須要對Iterator對象加鎖。
可是不能由於他是權威,咱們就全盤接受吧?
因此咱們眼見爲實,先把手冊裏面提到的【正例代碼】跑一下,以下:
細心的讀者可能發現了:咦,這個代碼的22行爲啥顏色不同呢?
我幫你看看。
替換以後的代碼是這樣的:
從上面咱們能夠獲得一個結論.......
等等,到這一步你就想獲得結論了?你不對【一行代碼爲何就替換了七行代碼】好奇嗎?
看到真相的時候,有時候再往前一步就是本質了。
源碼之下無祕密,我再送你一張圖,JDK1.8中Collection.removeIf的源碼:
好了,已經到源碼級別了,從這裏咱們驗證了,阿里java開發手冊裏面的正例是對的,並且我還想給他加上一句:
若是你的JDK版本是1.8以上,沒有併發訪問的狀況下,可使用Collection.removeIf(Predicate<? super E> filter)方法。使代碼更加優雅。
接下來咱們看看【反例代碼】的運行結果:
從執行結果來看,和咱們預期的結果是一致。看着沒有問題呀?
可是你別忘了,下面還有一句話啊:
咱們執行試一試:
什麼狀況?真的是"出乎意料"啊!
把刪除元素的條件從【公衆號】修改成【why技術】就發生了異常:
java.util.ConcurrentModificationException
咱們如今明白爲何阿里強制要求不要在foreach循環裏面進行元素的remove/add操做,由於會發生異常了。
可是開發手冊裏面並無告訴你,爲何會發生異常。須要咱們本身層層深刻,積極探索。
因此這一小節咱們就一塊兒探索,爲何會發生異常。咱們再解析一下程序的運行結果,以下:
正如上圖裏面異常信息的體現,異常是在代碼的第21行觸發的。而代碼的第21行,是一個foreach循環。foreach循環是Java的語法糖,咱們能夠從編譯後的class文件中看出,以下圖所示:
請注意圖中的第26行代碼:
list.remove(item) (這句話很關鍵!!!)
很關鍵,很重要,後面會講到。
這也解釋了,異常信息裏面的這一個問題:
好了,到這一步,咱們把異常信息都解讀完畢了。
我再看看真實拋出異常的那一個方法:
很簡單,很清晰的四行代碼。拋出異常的條件是:
modCount !=expectedModCount
因此,咱們須要解開的下兩層面紗就是下面兩大點:
第一:什麼是modCount?它是幹啥的?何時發生變化?
第二:什麼是expectedModCount?它是幹啥的?何時發生變化?
先來第一個:什麼是modCount?
modCount上的註釋很長,我只截取了最後一段。在這一段中,提到了兩個關鍵點。
1.modCount這個字段位於java.util.AbstractList抽象類中。
2.modCount的註釋中提到了"fail-fast"機制。
3.若是子類但願提供"fail-fast"機制,須要在add(int,E)方法和remove(int)方法中對這個字段進行處理。
4.從第三點咱們知道了,在提供了"fail-fast"機制的容器中(好比ArrayList),除了文中示例的remove(Obj)方法會致使ConcurrentModificationException異常,add及其相關方法也會致使異常。
知道了什麼是modCount。那modCount是幹啥的呢?
在提供了"fail-fast"機制的集合中,modCount的做用是記錄了該集合在使用過程當中被修改的次數。
證據就在源碼裏面,以下:
這是java.util.ArrayList#add(int, E)方法的源碼截圖:
這是java.util.ArrayList#remove(int)方法的源碼截圖:
注:這裏不討論手動設置爲null是否對GC有幫助,我我的認爲,在這裏有這一行代碼並無壞處。在實際開發過程當中,通常不須要考慮到這點。
同時,上面的源碼截圖也回答了這一層的最後一個問題:它何時被修改?
拿ArrayList來講,當調用add相關和remove相關方法時,會觸發modCount++操做,從而被修改。
好了,經過上面的分析,咱們知道了什麼是modCount和modCount是幹啥的。準備進入第四層。
接下來:什麼是expectedModCount?
expectedModCount是ArrayList中一個名叫Itr內部類的成員變量。
第二問:expectedModCount它是幹啥的:
它表明的含義是在這個迭代器中,預期的修改次數
第三問:expectedModCount何時發生變化?
狀況一:從上圖中也能夠看出當Itr初始化的時候,會對expectedModCount字段賦初始值,其值等於modCount。
狀況二:以下圖所示,調用Itr的remove方法後會再次把modCount的值賦給expectedModCount。
換句話說就是:調用迭代器的remove會維護expectedModCount=modCount。(這句話很關鍵!!!)
好了分析到了這裏,咱們知道了下面這個六連擊:
1.什麼是modCount?
2.modCount是幹啥的?
3.modCount何時發生變化?
4.什麼是expectedModCount?
5.expectedModCount是幹啥的?
6.expectedModCount何時發生變化?
爲何發生了異常呢?
若是說前四層是線索的話,真相其實已經隱藏在線索裏面了。我帶你梳理一下:
【第一層:異常信息解讀】中說到:
【第二層:拋出異常的條件解讀】中說到:
【第三層:什麼是modCount?它是幹啥的?何時發生變化?】中說到:
【第四層:什麼是expectedModCount?它是幹啥的?何時發生變化?】中說到:
爲何發生了異常呢?我想你大概已經有了一個答案了,我再去Debug一下,爲了方便演示,咱們去掉語法糖,程序修改以下:
並確認一下這個循環體會執行三次,以下:
第一次循環
第一次循環取出的【公衆號】,不知足條件if("why技術".equals(item)),不會觸發list.remove(Obj)方法。
第二次循環
如圖所示,第二次循環取到了「why技術」。知足條件if("why技術".equals(item)),會觸發list.remove(Obj)方法,以下所示:
第三次循環
總結一下在foreach循環裏面進行元素的remove/add操做拋出異常的真相:
由於foreach循環是Java的語法糖,通過編譯後還原成了迭代器。
可是從通過編譯後的代碼的第26行能夠看出,remove方法的調方是list,而不是迭代器。
通過前面的源碼分析咱們知道,因爲ArrayList的"fail-fast"機制,調用remove方法會觸發【modCount++】操做,對expectedModCount沒有任何操做。只有調用迭代器的remove方法,纔會維護expectedModCount=modCount。
因此調用了list的remove方法後,再調用Itr的next方法時,致使了expectedModCount!=modCount,拋出異常。
前面講了阿里開發手冊。講了在foreach循環裏面進行元素的remove/add爲何會發生異常。有了這些鋪墊以後。
咱們再回過頭來看小馬哥出的這個題:
我靠,這乍一看,foreach循環裏面調用list.remove(obj)。咱們剛剛分析過,會拋出ConcurrentModificationException異常。
你要這樣答,你就進了小馬哥的坑了。
這個題的坑在這三個點裏面。小馬哥並無說這個list是ArrayList吧?若是你沒有認真審題,先入爲主的默認了這個list就是ArrayList。第一步就錯了。
這是真正的高手,借力打力。借阿里開發手冊的力,讓你第一步就走錯。
請看下面這張圖:
當使用CopyOnWriteArrayList的時候,程序正常執行。
既然咱們知道爲何會拋出異常,也知道怎麼不拋出異常,List原本就是一個接口,那咱們是否是能夠實現這個接口,弄一個自定義的List呢?
好比下面的這個WhyTechnologyList,就是我本身的List,狸貓換太子,這操做,夠"騷"啊。
只有掌握了原理,咱們想怎麼玩就怎麼玩。
CopyOnWriteArrayList是什麼?咱們看一下源碼註釋上面是怎麼說的:
相對於ArrayList而言,CopyOnWriteArrayList集合是線程安全的容器。在遍歷的時候,因爲它操做是數組的"快照","快照"不會發生變化。因此它不須要額外加鎖,也不會拋出ConcurrentModificationException異常。
咱們主要看一下,示例程序中用到的三個方法,add(E e)、next()、remove(Obj)
先看add(E e)方法:
咱們看一下它的next()方法:
再看一下它的remove(Obj)方法:
next、remove都是操做的快照,並無看到ArrayList裏面的modCount和expectedModCount。因此它沒有拋出ConcurrentModificationException
以前看小馬哥說的這句話的時候還不太明白集合和一致性之間的關係(老問題,仍是先入爲主,一說到一致性首先想到的是緩存和數據庫之間的一致性)。
可是當我閱讀源碼,從add方法能夠看出CopyOnWriteArrayList並不保證數據的實時一致性。只能保證最終一致性。
同時咱們從源碼中能夠看出CopyOnWriteArrayList增刪改數據的時候須要搞一個"快照",這一點是比較耗內存的,使用過程當中須要注意。
咱們再回到最開始的地方,看看你們的回答:
1.什麼也不會發生,remove以後,list中的數據會被清空。
2.remove的方法調用錯誤,入參應該是index(數組下標)。
3.併發操做的時候會出現異常。
4.會發生ConcurrentModifyException。
如今,你知道這些回答的問題在哪裏了吧?這一部分的總結也很簡單,上一個對比圖就行了:
如今面試官常常問的一個問題,你讀過源碼嗎?
咦,巧了。你看了這篇文章,就至關於了讀了ArrayList和CopyOnWriteArrayList的部分源碼。
那你就能夠這樣回答啦:我以前看阿里Java開發手冊的時候看到一條規則是
不要在foreach循環裏面進行元素的remove/add操做。remove元素請使用Iterator方式,若是併發操做,須要對Iterator對象加鎖。
我對這條規則很感興趣,因此我對其進行了深刻的研究,閱讀了
ArrayList和CopyOnWriteArrayList的部分源碼。
若是碰巧面試官也讀過這塊源碼,這個問題,大家能夠相談甚歡。
若是面試官沒有讀過這塊源碼,你能夠給他講的明明白白。
固然,還有一個前提是:我但願你讀完這篇文章後,若是是第一次知道這個知識點,那你能夠本身實際操做一下。
看懂了是一回事,本身再實際操做一下,是另一回事。
文中屢次提到了"fail-fast"機制(快速失敗),與其對應的還有"fail-safe"機制(失敗安全)。
這種機制是一種思想,它不只僅是體如今Java的集合中。在咱們經常使用的rpc框架Dubbo中,在集羣容錯時也有相關的實現。
Dubbo 主要提供了這樣幾種容錯方式:
Failover Cluster - 失敗自動切換
Failfast Cluster - 快速失敗
Failsafe Cluster - 失敗安全
Failback Cluster - 失敗自動恢復
Forking Cluster - 並行調用多個服務提供者
若是對這兩種機制感興趣的朋友能夠查閱相關資料,進行了解。若是想要了解Dubbo的集羣容錯機制,能夠看官方文檔,地址以下:
http://dubbo.apache.org/zh-cn...
文中說到foreach循環的時候提到了Java的語法糖。若是對這一塊有興趣的讀者,能夠在網上查閱相關資料,也能夠看看《深刻理解Java虛擬機》的第10.3節,有專門的介紹。
書中說到:
總而言之,語法糖能夠看作是編譯器實現的一些「小把戲」,這些「小把戲」可能會使得效率「大提高」,但咱們也應該去了解這些「小把戲」背後的真實世界,那樣才能利用好它們,而不是被它們所迷惑。
關注公衆號並回復關鍵字【Java】。便可得到此書的電子版。
阿里的孤盡大佬做爲主要做者寫的這本《阿里Java開發手冊》,能夠說是嘔心瀝血推出的業界權威,很是值得閱讀。讀完此書,你不只可以得到不少乾貨,甚至你還能讀出一點技術情懷在裏面。
對於技術情懷,孤盡大佬是這樣的說的:
熱愛、思考、卓越。熱愛是一種源動力,而思考是一個過程,而卓越是一個結果。若是給這三個詞加一個定語,使技術情懷更加立體、清晰地被解讀,那就是奉獻式的熱愛,主動式的思考,極致式的卓越。
關注公衆號並回復關鍵字【Java】。便可得到此書的電子版。
這篇文章寫以前我一直在糾結,由於感受這個知識點其實我已經掌握了,那我還有寫的必要嗎?我在寫的這個過程當中還能收穫一些東西嗎?
可是在寫的過程當中,我翻閱了大量的源碼,雖然以前已經看過,可是沒有這樣一行一行仔細的去分析。以前只是一個大概的模糊的影像,如今具象化清晰了起來,在這個過程當中,我仍是學到了不少不少。
其實想到寫什麼內容並不難,難的是你對內容的把控。關於技術性的語言,我是反覆推敲,查閱大量文章來進行證僞,總之慎言慎言再慎言,畢竟作技術,我認爲是一件很是嚴謹的事情,我經常想象本身就是在故宮修文物的工匠,在工匠精神的認知上,目前我可能和他們還差的有點遠,可是我時常以工匠精神要求本身。就像我以前表達的:對於技術文章(由於我偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評),我儘可能保證周推,全力保證質量。
文中提到的兩本書《深刻理解Java虛擬機》和《阿里Java開發手冊》是兩本很是優秀,值得反覆閱讀的工具書,能夠關注我後,在後臺發送java,便可得到電子書。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
若是你以爲文章還不錯,你的點贊、留言、轉發、分享、讚揚就是對我最大的鼓勵。
另外,若是小馬哥本尊能讀到這個文章,讀到這段話,我想在這裏表達對他的敬意。同時也想催更一下:小馬哥,每日一問很久沒更新啦,很是懷戀那種被"坑"的明明白白的感受!
以上。
謝謝您的閱讀,感謝您的關注。
歡迎關注公衆號【why技術】。在這裏我會分享一些技術相關的東西,主攻java方向,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評。願你我共同進步。