ConcurrentModificationException日志關(guān)鍵字報(bào)警思考分析
一、背景
近期,在日常的日志關(guān)鍵字報(bào)警分析時(shí),發(fā)現(xiàn)我負(fù)責(zé)的一個(gè)電商核心系統(tǒng)在某時(shí)段存在較多ConcurrentModificationException異常日志,遂進(jìn)行分析和改進(jìn),下面是我的一些思考。
1.1 系統(tǒng)架構(gòu)
一直以來(lái),無(wú)狀態(tài)的服務(wù)都被當(dāng)作分布式服務(wù)設(shè)計(jì)的最佳實(shí)踐。因?yàn)闊o(wú)狀態(tài)的服務(wù)對(duì)于擴(kuò)展性和運(yùn)維方面有著得天獨(dú)厚的優(yōu)勢(shì),可以隨意地增加和減少節(jié)點(diǎn)。本系統(tǒng)的整體架構(gòu)可以認(rèn)為是由一個(gè)MQ應(yīng)用、一個(gè)RPC應(yīng)用和底層存儲(chǔ)組成。
RPC應(yīng)用是無(wú)狀態(tài)服務(wù),對(duì)外提供常用的查詢和操作接口;采用雙機(jī)房部署,每個(gè)機(jī)房10*8C16G;
MQ應(yīng)用是無(wú)狀態(tài)服務(wù),負(fù)責(zé)消費(fèi)MQ消息,在消費(fèi)過(guò)程中會(huì)調(diào)用該RPC應(yīng)用提供方法;采用雙機(jī)房部署,每個(gè)機(jī)房5*8C16G;
底層存儲(chǔ)用的是數(shù)據(jù)庫(kù)集群和緩存集群,大概如圖所示:
1.2 關(guān)鍵代碼
MyRpcService
對(duì)外提供RPC服務(wù),getList
方法可以根據(jù)入?yún)⒅械臓顟B(tài)進(jìn)行查詢,由于業(yè)務(wù)需要,需要對(duì)入?yún)⒌臓顟B(tài)進(jìn)行排序,實(shí)現(xiàn)部分關(guān)鍵代碼如下:
public class MyRpcServiceImpl implements MyRpcService{ @Override public BaseResult getList(ListParam listParam) { BaseResult baseResult = new BaseResult(); List<Integer> states = listParam.getStateList(); // 省略大段代碼 KeyUtil.getKeyString(states); // 省略大段代碼 baseResult.setSuccess(true); return baseResult; } }
KeyUtil
是一個(gè)工具類,getKeyString
方法對(duì)入?yún)⒌?code>itemList進(jìn)行排序使用的是Java集合框架內(nèi)置的sort 方法,代碼如下:
public class KeyUtil { public static String getKeyString(List<Integer> itemList) { String result = ""; //省略代碼 Collections.sort(itemList); //省略代碼 return result; } }
MyMqConsumer
是MQ消費(fèi)者,負(fù)責(zé)監(jiān)聽消息進(jìn)行消費(fèi)。在消費(fèi)邏輯中,會(huì)調(diào)用MyRpcService
的getList()
方法進(jìn)行狀態(tài)查詢,因?yàn)椴樵兊臓顟B(tài)是固定的,所以在Consumer
類中定義了static final
類型的stateList
,關(guān)鍵代碼如下:
public class MyMqConsumer implements MessageListener{ public static final List<Integer> stateList = Stream.of(1).collect(Collectors.toList()); @Resource private MyRpcService myRpcService; @Override public void onMessage(List<Message> messageList) { // 省略代碼 for (Message message : messageList) { // 省略其他代碼 ListParam listParam = new ListParam(); listParam.setStateList(stateList); BaseResult result = myRpcService.getList(listParam); // 省略其他代碼 } } }
二、原因分析
看了上面的系統(tǒng)架構(gòu)和關(guān)鍵代碼,不知道你有沒有發(fā)現(xiàn)問題?可以先拋開設(shè)計(jì)和代碼實(shí)現(xiàn)方面的問題不談,只看這樣的代碼能不能正常執(zhí)行,得到正確的業(yè)務(wù)結(jié)果。
既然這么問了,當(dāng)然會(huì)有問題:在高并發(fā)環(huán)境下,MQ應(yīng)用在消費(fèi)消息時(shí),調(diào)用RPC服務(wù)查詢時(shí)可能會(huì)拋出異常,從而觸發(fā)MQ異常重試,至于對(duì)業(yè)務(wù)有沒有影響,得具體問題具體分析了。
ERROR 執(zhí)行流程時(shí)出錯(cuò) java.util.ConcurrentModificationException:null at java.util.ArrayList.forEach(ArrayList.java:1260)~[:?1.8.0_192] at com.shangguan.test.util.KeyUtil.getKeyString(KeyUtil.java:10) ...
2.1 ArrayList源碼
從日志中可以看到,ConcurrentModificationException
是java.util.ArrayList
類里面的forEach
方法拋出來(lái)的,源碼如下:
@Override public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); final int expectedModCount = modCount; @SuppressWarnings("unchecked") final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
在該方法中,內(nèi)部會(huì)維護(hù)一個(gè)expectedModCount
變量,賦值為modCount
,在每次迭代過(guò)程中,迭代器會(huì)檢查expectedModCount
是否等于當(dāng)前的modCount
。如果不等,說(shuō)明在迭代過(guò)程中ArrayList
的結(jié)構(gòu)發(fā)生了修改,迭代器會(huì)拋出ConcurrentModificationException
異常。這種設(shè)計(jì)可以確保在多線程環(huán)境下,當(dāng)一個(gè)線程修改ArrayList
時(shí),其他線程在迭代過(guò)程中可以立即發(fā)現(xiàn)這種修改,從而避免潛在的數(shù)據(jù)不一致問題。
再可以看下源碼中modCount
的注釋,大意是:
modCount
表示ArrayList
自從創(chuàng)建以來(lái)結(jié)構(gòu)上發(fā)生的修改次數(shù)。結(jié)構(gòu)修改是指改變列表大小的修改,或者以其他方式擾亂列表,使正在進(jìn)行的迭代可能產(chǎn)生不正確的結(jié)果。
modCount
字段用于iterator
和listIterator
方法返回的迭代器(或列表迭代器)。如果這個(gè)字段的值在迭代過(guò)程中發(fā)生意外的變化,迭代器(或列表迭代器)將在next、remove、previous、set或add
操作時(shí)拋出ConcurrentModificationException
異常。這提供了fail-fast(快速失敗)行為,而不是在迭代過(guò)程中遇到并發(fā)修改時(shí)具有不確定性。
子類可以選擇使用這個(gè)字段。如果子類希望提供fail-fast迭代器(和列表迭代器),那么它只需在其add(int, E)
和remove(int)
方法(以及覆蓋的任何其他導(dǎo)致列表結(jié)構(gòu)修改的方法)中遞增此字段。單次調(diào)用add(int, E)
或remove(int)
應(yīng)該在此字段上增加不超過(guò)1次,否則迭代器(和列表迭代器)將拋出虛假的ConcurrentModificationException
。如果實(shí)現(xiàn)不希望提供fail-fast迭代器,可以忽略此字段。
2.2 線程安全問題
有個(gè)有趣的現(xiàn)象是,這個(gè)異常日志僅存在MQ應(yīng)用中,這是為什么呢?
這其實(shí)是一個(gè)多線程問題。我們知道,static對(duì)象是在類加載時(shí)創(chuàng)建的全局對(duì)象,它們的生命周期與類的生命周期相同。static對(duì)象在程序啟動(dòng)時(shí)創(chuàng)建,在程序結(jié)束時(shí)銷毀。這意味著static對(duì)象在多個(gè)線程之間共享的,可能存在線程安全問題。
翻回去仔細(xì)看下代碼,可以看到MyMqConsumer
定義的stateList是static類型的,是否是否存在線程安全問題呢?
在流量較低的情況下,多個(gè)消息不在同一時(shí)刻到達(dá),每個(gè)線程處理消息將不會(huì)爭(zhēng)奪static對(duì)象,所以不會(huì)有問題;
當(dāng)流量較大情況下,有多個(gè)消息可能在同一時(shí)刻到達(dá),每個(gè)線程處理過(guò)程中都會(huì)對(duì)stateList進(jìn)行賦值,調(diào)用遠(yuǎn)程RPC接口,它們之間將會(huì)爭(zhēng)奪static對(duì)象,可能存在問題。例如上圖中右半部分,線程1還沒有處理完消息1時(shí),線程2就開始爭(zhēng)搶,那么就可能使ArrayList中modCount != expectedModCount條件滿足,從而拋出異常。
三、改進(jìn)思考
3.1 本問題的優(yōu)化
經(jīng)過(guò)上述分析,已經(jīng)清楚問題的產(chǎn)生原因了。對(duì)于本問題的優(yōu)化,其實(shí)也比較簡(jiǎn)單。有如下兩種方式可供選擇:
1. 在MyMqConsumer
調(diào)用RPC查詢的入?yún)ⅲ褂?strong>new List來(lái)替代原來(lái)的類中定義好的static對(duì)象;
2. 修改KeyUtil
代碼,淺拷貝傳入的itemList,再進(jìn)行排序
3.2類似問題的發(fā)現(xiàn)和改進(jìn)
本問題已經(jīng)修復(fù),那類似的問題是否可以避免或者減少,將是接下來(lái)值得思考的一個(gè)問題。為了減少這類問題發(fā)生,我結(jié)合平時(shí)工作過(guò)程中的幾個(gè)階段,認(rèn)為可以從以下幾個(gè)方面進(jìn)行改進(jìn):
- 開發(fā)
開發(fā)過(guò)程中,開發(fā)人員需要提升認(rèn)知和水平,注意代碼中可能存在的線程問題;注意編寫單元測(cè)試,可以通過(guò)模擬多線程環(huán)境來(lái)檢測(cè)潛在的問題。
- 代碼評(píng)審
開發(fā)完成的代碼一定需要進(jìn)行代碼評(píng)審,評(píng)審過(guò)程中架構(gòu)師需要發(fā)揮自己豐富的開發(fā)經(jīng)驗(yàn)和較強(qiáng)的代碼直覺,“火眼金睛”,發(fā)現(xiàn)代碼中的漏洞;當(dāng)然這對(duì)評(píng)審人員的要求很高,因?yàn)閮H通過(guò)改動(dòng)的幾行代碼發(fā)現(xiàn)問題確實(shí)是一件很有挑戰(zhàn)的事情。如果要有一些自動(dòng)化工具或者插件,則可以起到事半功倍的效果。這里其實(shí)我還沒有調(diào)研相關(guān)的工具,如果有大佬有相關(guān)經(jīng)驗(yàn)歡迎評(píng)論交流。
- 測(cè)試
測(cè)試階段除了驗(yàn)證正常的業(yè)務(wù)功能,還需要進(jìn)行集成測(cè)試和性能測(cè)試。在集成測(cè)試中,將多個(gè)模塊組合在一起,測(cè)試整個(gè)系統(tǒng)在多線程環(huán)境中的行為,有助于發(fā)現(xiàn)模塊之間的交互問題。除了繼承測(cè)試,有時(shí)還需要性能測(cè)試,性能測(cè)試可以發(fā)現(xiàn)潛在的競(jìng)爭(zhēng)條件、死鎖、資源爭(zhēng)用等多線程問題。
四、小結(jié)
最后,我簡(jiǎn)單總結(jié)一下本文內(nèi)容。本文主要記錄和分析日志中的ConcurrentModificationException關(guān)鍵字報(bào)警,首先介紹了系統(tǒng)整體架構(gòu)和關(guān)鍵代碼;然后從ArrayList源碼和線程安全兩個(gè)方面分析問題產(chǎn)生原因,最后我提出了修復(fù)該問題的方案和類似問題的思考,希望對(duì)大家有幫助。
以上就是ConcurrentModificationException日志關(guān)鍵字報(bào)警思考分析的詳細(xì)內(nèi)容,更多關(guān)于ConcurrentModificationException報(bào)警的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java獲取當(dāng)前時(shí)間并轉(zhuǎn)化為yyyy-MM-dd?HH:mm:ss格式的多種方式
這篇文章主要介紹了Java獲取當(dāng)前時(shí)間并轉(zhuǎn)化為yyyy-MM-dd?HH:mm:ss格式的多種方式,每種方式結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-03-03如何解決Mybatis-plus中@TableLogic注解失效問題
這篇文章主要介紹了如何解決Mybatis-plus中@TableLogic注解失效問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05Eclipse+Java+Swing+Mysql實(shí)現(xiàn)工資管理系統(tǒng)
這篇文章主要介紹了Eclipse+Java+Swing+Mysql實(shí)現(xiàn)工資管理系統(tǒng),對(duì)正在工作或者學(xué)習(xí)的你有一定的參考價(jià)值,需要的朋友可以參考一下2022-01-01詳解Spring Cloud 跨服務(wù)數(shù)據(jù)聚合框架
這篇文章主要介紹了詳解Spring Cloud 跨服務(wù)數(shù)據(jù)聚合框架,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03