一、問題背景
最近在分析一個客戶的實際問題中,從日誌中看到了一個類似如下的異常(圖A),實際異常棧信息量較多,由於涉及到產品代碼,所以不便在此貼出,圖A是異常棧的最底層拋異常的原因;程序並沒有對最上層那行業務代碼做try{}catch(),而該功能是會與第三方系統交互的,在最開始設計該功能時是分兩大塊處理的,每塊都有一個大的事務管理;因此該異常直接跑飛沒能反寫第一個事務已提交的數據,導致該業務數據對象狀態不正確,從而只能通過在後臺修改讓業務人員在重新操作,實際業務場景不便在此贅述。
圖A
二、要解決的問題
問題本身是比較容易解決的,catch處後進行反寫操作,在拋 出異常即可,而真正要弄明白的問題是:
1、這個異常是什麼、通常是什麼場景下可能會出現?
2、該異常是否真的無法重現?
3、拋該異常的本質原因是什麼?拋這個異常的邏輯是?
4、能否用一段小程序通過調試的方式模擬重現?
5、產品當時併發是滿足了哪些因素,程序走到哪,進行了何種操
作這麼巧發生了?
三、解決疑問
(1)這個異常是什麼、通常是什麼場景下可能會出現?
百度可知該異常已屢見不鮮,不管是單線程還是多線程的情況都有,比如單線程下一個實際類型爲ArrayList的集合list裏面添加了5個元素,現對list進行迭代,中間做一個if判斷,判斷條件如迭代到第3個元素時,通過list.remove(obj)方式移除對象,在繼續迭代時就會報該異常,這種場景是由代碼編寫有誤導致,正確做法應該是通過迭代器移除對象。
而多線程的場景,簡單看了其中一個示例則是把list定義爲靜態屬性,被不同線程操作併發導致的,比如一個線程正在對這個list做迭代操作,另一個線程通過list在做remove操作,這種情形也是有可能發生的
(2)該異常是否真的無法重現?
分析日誌後,已可以準確判斷是需要做數據修復的,修改其中一筆後,重新操作ok了,後將這些筆數據狀態修改後批量一次操作也是ok的,說明這是偶發現象,因爲平常客戶使用時也一直沒有問題。
基於該異常的場景及該功能後臺代碼的分析,可以肯定是併發導致。
爲啥這麼肯定?是不是看錯日誌了?
1、通過客戶端操作日誌,服務端日誌和後臺數據發生時間,這三者的時間是極其吻合的,並且報的異常棧 信息也是對應這個功能的,從業務者的角度操作 該業務數據對象,現象也是相符的。
2、當時客戶操作的筆數是33筆,大於10筆客戶端會啓動多個線程的。
3、日誌已鐵證如山的表明當時拋了異常。
可以想象當時一個線程在迭代HashMap中的元素時,另外一個線程對 HashMap做了某個操作導致,比如 remove操作;因此根據異常堆棧中涉及的流程性代碼,仔細的檢查了一遍,並沒有把map定義爲全局變量,也 沒有remove操作,只看到了list.addAll(<Map.Entry<Object,Object>>),並且Map.Entry<Object,Object>不 是靜態的,而是一個典型的成員變量.
因此需要看HashMap的源碼仔細分析拋該異常的本質原因,是具體滿足了哪些因素後纔會拋該異常。
(3)拋該異常的本質原因是什麼?拋這個異常的邏輯是?
查看HashMap相關源碼可知,當迭代器迭代的時,會判斷modCount的值和expectedModCount的值是否相 等;如果相等則往下進行,如果不等則拋該異常,因此該異常的本質原因是這兩個值不等導致的。
那麼modCount和expectedModCount這兩個值是在哪定義的呢?什麼時候發生變化的呢?
分析調試源碼知:modCount是HashMap中的成員變量,expectedModCount是HashIterator中的一個成員變量,並且HashIterator是HashMap的內部類;搜索modCount在HashMap中引用可知,put、remove等操會讓modCount的值發生變化,而且變化指的就是自加1,從英文的表面意思上可以猜出這個變量設計的業務語義是表示HashMap修改的次數,所以只要HashMap這個容器往裏放對象或者是移除對象導致HashMap結構發生變化的操作都會使modCount的值就會加1,而迭代操作不會。
expectedModCount的值則是在初始化迭代器對象HashIterator時,在構造函數就進行了modCount = expectedModCount的賦值,其次,是在使用迭代器移除map中的元素時又進行了賦值動作,在方法的結尾會將modCount賦值給expectedModCount,因爲HashIterator迭代器移除元素的具體實現就是通過調用了HashMap的移除方法,此時modCount會+1,因此對expectedModCount進行了重新賦值;而這是爲什麼在迭代過程中通過迭代器移除map元素不會報此異常,通過map本身的方式移除元素會報此異常的原因。
(4)能否用一段小程序通過調試的方式模擬重現
已模擬出,具體代碼不在此貼出了。
四、場景還原
通過上述分析和模擬程序可知,當時拋該異常應該同時滿足如下3點:
1、必須操作的是同一個map對象
2、其中一個線程肯定在執行到了判斷modCount值與 expectedModCount是否相等此處(日誌可知此時是在做迭 代操作,此條件已滿足)
3、另外一個線程同樣拿到該map對象進行了put,或者remove操作,導致modCount值發生了變化
業務代碼並沒有寫,爲什麼會有迭代操作?
從日誌分析可知是list在addAll時產生的,這是sun實現addAll方法時產生的。
查看ArrayList中的addAll(Collection c)可知,實現Collection接口的類的對象都可以作爲參數傳入該方法 中,在ArrayList的addAll方法中,首先是要將c轉換成一個對象數組,Collection接口定義了該方法 toArray(),讓實現類去實現,在運行期在去真正調用實現類的該方法,通過日誌可知,我們傳進去的對象是 HashMap的內部類EntrySet<Map.Entry<Object,Object>>對象,而EntrySet是實現了Collection接口的,事實 上內部類EntrySet本身沒有重寫toArray()方法,而是他繼承的父類的父類AbstractCollection實現了該方 法, 具體如圖B;在AbstractCollection的toArray方法中首先是創建了一個數組對象,而其本身定義了一 個獲取迭 代器的抽象方法iterator,在運行期動態調用真正子類的iterator創建迭代器,具體如圖C
圖B(AbstractCollecton實現toArray)
圖C(子類創建迭代器)
繼續看源碼可知,newEntryIterator 是創建了一個EntryIterator的迭代器對象,完成其父類和自己的初 始化工作,而EntryIterator是繼承自HashIterator的,並且這兩個類都是HashMap的內部類,HashIterator實 現了Iterator接口,因此在初始化EntryIterator時就連帶着初始化HashIterator並且是先初始化 HashIterator;在初始化HashIterator時expectedModCount就被賦值了,取的是當前HashMap對象的 modCount,modCount的值此時不一定是0,假如該對象已經put了2個值,那麼此時 expectedModCount=modCount=2,如expectedModCount初始化被賦值代碼如圖D
圖D
方法調用棧如圖E
圖E
(說明:<init>是對象初始化方法,<clinit>是存放靜態屬性或者方法塊的方法,這兩個方法是編譯器在編譯的時候產生的,具體是什麼條件下產生的在此就不在贅述了)。
AbstractCollection拿到迭代器後,就開始迭代,將HashMap中的元素一個一個迭代出來保存到對象數組中,所以此時另外一個線程拿到該map對象,正好進行put操作,此時就會拋異常了。
通過上述分析可以明白,list.addAll爲啥會產生知迭代操作了。
那麼是如何傳同一個map對象的呢?又是在什麼時候進行put操作的呢?
分析調試相關基礎框架代碼可知,是同一個上下文導致了同一個map;基礎框架中通過SessionManager 來管理SessionInfo,存放SessionInfo的容器實際上是一個final static HashMap<String,SessionInfo>,不同 線程則是通過該對象進行信息交換和傳遞的;而上下文context是作爲SessionInfo的成員變量存在的,並且用 final進行修飾了;而我們最終關心的entrySet對應的Map則來於Context的一個普通的成員變量;因此不同線 程通過同一個sessionid就可以拿到同一個session,最終操作同一個map對象了。
經實驗(調試)可知,在不退出客戶端的情況下,sessionid是相同的,並且多線程提交到服務端後取到 的sessionid,sessionInfo對象也是相同的,此處並沒有深究sessionid爲啥是相同的,但從業務語義來講在 客戶端未關閉前,每次拿到的sessionid是一樣的是比較合理。
分析至此,離真相越來越近,下面則是什麼時候進行的put操作。
從Context裏可知有一個put方法,而該方法是在unmarashel中調用,unmarshel則又是通過rpc調用遠程方 法 的時候調用,根據日誌的異常棧信息可知,當時確實進行了遠程調用,所以當時是一個線程調用遠程 方法後進行addAll中的迭代操作時,另一個線程也在調用遠程方法進行unmarashel的過程中進行了put操作, 導致併發產生,出現了該異常。
一切都是那麼不可思議,但確實又發生了。