ConcurrentModificationException日志關(guān)鍵字報警思考分析
一、背景
近期,在日常的日志關(guān)鍵字報警分析時,發(fā)現(xiàn)我負(fù)責(zé)的一個電商核心系統(tǒng)在某時段存在較多ConcurrentModificationException異常日志,遂進(jìn)行分析和改進(jìn),下面是我的一些思考。
1.1 系統(tǒng)架構(gòu)
一直以來,無狀態(tài)的服務(wù)都被當(dāng)作分布式服務(wù)設(shè)計的最佳實(shí)踐。因?yàn)闊o狀態(tài)的服務(wù)對于擴(kuò)展性和運(yùn)維方面有著得天獨(dú)厚的優(yōu)勢,可以隨意地增加和減少節(jié)點(diǎn)。本系統(tǒng)的整體架構(gòu)可以認(rèn)為是由一個MQ應(yīng)用、一個RPC應(yīng)用和底層存儲組成。
RPC應(yīng)用是無狀態(tài)服務(wù),對外提供常用的查詢和操作接口;采用雙機(jī)房部署,每個機(jī)房10*8C16G;
MQ應(yīng)用是無狀態(tài)服務(wù),負(fù)責(zé)消費(fèi)MQ消息,在消費(fèi)過程中會調(diào)用該RPC應(yīng)用提供方法;采用雙機(jī)房部署,每個機(jī)房5*8C16G;
底層存儲用的是數(shù)據(jù)庫集群和緩存集群,大概如圖所示:

1.2 關(guān)鍵代碼
MyRpcService 對外提供RPC服務(wù),getList 方法可以根據(jù)入?yún)⒅械臓顟B(tài)進(jìn)行查詢,由于業(yè)務(wù)需要,需要對入?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 是一個工具類,getKeyString 方法對入?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)邏輯中,會調(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è)計和代碼實(shí)現(xiàn)方面的問題不談,只看這樣的代碼能不能正常執(zhí)行,得到正確的業(yè)務(wù)結(jié)果。
既然這么問了,當(dāng)然會有問題:在高并發(fā)環(huán)境下,MQ應(yīng)用在消費(fèi)消息時,調(diào)用RPC服務(wù)查詢時可能會拋出異常,從而觸發(fā)MQ異常重試,至于對業(yè)務(wù)有沒有影響,得具體問題具體分析了。
ERROR 執(zhí)行流程時出錯 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方法拋出來的,源碼如下:
@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)部會維護(hù)一個expectedModCount變量,賦值為modCount,在每次迭代過程中,迭代器會檢查expectedModCount是否等于當(dāng)前的modCount。如果不等,說明在迭代過程中ArrayList的結(jié)構(gòu)發(fā)生了修改,迭代器會拋出ConcurrentModificationException異常。這種設(shè)計可以確保在多線程環(huán)境下,當(dāng)一個線程修改ArrayList時,其他線程在迭代過程中可以立即發(fā)現(xiàn)這種修改,從而避免潛在的數(shù)據(jù)不一致問題。
再可以看下源碼中modCount的注釋,大意是:
modCount表示ArrayList自從創(chuàng)建以來結(jié)構(gòu)上發(fā)生的修改次數(shù)。結(jié)構(gòu)修改是指改變列表大小的修改,或者以其他方式擾亂列表,使正在進(jìn)行的迭代可能產(chǎn)生不正確的結(jié)果。
modCount字段用于iterator和listIterator方法返回的迭代器(或列表迭代器)。如果這個字段的值在迭代過程中發(fā)生意外的變化,迭代器(或列表迭代器)將在next、remove、previous、set或add操作時拋出ConcurrentModificationException異常。這提供了fail-fast(快速失?。┬袨?,而不是在迭代過程中遇到并發(fā)修改時具有不確定性。
子類可以選擇使用這個字段。如果子類希望提供fail-fast迭代器(和列表迭代器),那么它只需在其add(int, E)和remove(int)方法(以及覆蓋的任何其他導(dǎo)致列表結(jié)構(gòu)修改的方法)中遞增此字段。單次調(diào)用add(int, E)或remove(int)應(yīng)該在此字段上增加不超過1次,否則迭代器(和列表迭代器)將拋出虛假的ConcurrentModificationException。如果實(shí)現(xiàn)不希望提供fail-fast迭代器,可以忽略此字段。
2.2 線程安全問題
有個有趣的現(xiàn)象是,這個異常日志僅存在MQ應(yīng)用中,這是為什么呢?
這其實(shí)是一個多線程問題。我們知道,static對象是在類加載時創(chuàng)建的全局對象,它們的生命周期與類的生命周期相同。static對象在程序啟動時創(chuàng)建,在程序結(jié)束時銷毀。這意味著static對象在多個線程之間共享的,可能存在線程安全問題。
翻回去仔細(xì)看下代碼,可以看到MyMqConsumer定義的stateList是static類型的,是否是否存在線程安全問題呢?

在流量較低的情況下,多個消息不在同一時刻到達(dá),每個線程處理消息將不會爭奪static對象,所以不會有問題;
當(dāng)流量較大情況下,有多個消息可能在同一時刻到達(dá),每個線程處理過程中都會對stateList進(jìn)行賦值,調(diào)用遠(yuǎn)程RPC接口,它們之間將會爭奪static對象,可能存在問題。例如上圖中右半部分,線程1還沒有處理完消息1時,線程2就開始爭搶,那么就可能使ArrayList中modCount != expectedModCount條件滿足,從而拋出異常。
三、改進(jìn)思考
3.1 本問題的優(yōu)化
經(jīng)過上述分析,已經(jīng)清楚問題的產(chǎn)生原因了。對于本問題的優(yōu)化,其實(shí)也比較簡單。有如下兩種方式可供選擇:
1. 在MyMqConsumer調(diào)用RPC查詢的入?yún)?,使?strong>new List來替代原來的類中定義好的static對象;
2. 修改KeyUtil代碼,淺拷貝傳入的itemList,再進(jìn)行排序
3.2類似問題的發(fā)現(xiàn)和改進(jìn)
本問題已經(jīng)修復(fù),那類似的問題是否可以避免或者減少,將是接下來值得思考的一個問題。為了減少這類問題發(fā)生,我結(jié)合平時工作過程中的幾個階段,認(rèn)為可以從以下幾個方面進(jìn)行改進(jìn):
- 開發(fā)
開發(fā)過程中,開發(fā)人員需要提升認(rèn)知和水平,注意代碼中可能存在的線程問題;注意編寫單元測試,可以通過模擬多線程環(huán)境來檢測潛在的問題。
- 代碼評審
開發(fā)完成的代碼一定需要進(jìn)行代碼評審,評審過程中架構(gòu)師需要發(fā)揮自己豐富的開發(fā)經(jīng)驗(yàn)和較強(qiáng)的代碼直覺,“火眼金睛”,發(fā)現(xiàn)代碼中的漏洞;當(dāng)然這對評審人員的要求很高,因?yàn)閮H通過改動的幾行代碼發(fā)現(xiàn)問題確實(shí)是一件很有挑戰(zhàn)的事情。如果要有一些自動化工具或者插件,則可以起到事半功倍的效果。這里其實(shí)我還沒有調(diào)研相關(guān)的工具,如果有大佬有相關(guān)經(jīng)驗(yàn)歡迎評論交流。
- 測試
測試階段除了驗(yàn)證正常的業(yè)務(wù)功能,還需要進(jìn)行集成測試和性能測試。在集成測試中,將多個模塊組合在一起,測試整個系統(tǒng)在多線程環(huán)境中的行為,有助于發(fā)現(xiàn)模塊之間的交互問題。除了繼承測試,有時還需要性能測試,性能測試可以發(fā)現(xiàn)潛在的競爭條件、死鎖、資源爭用等多線程問題。
四、小結(jié)
最后,我簡單總結(jié)一下本文內(nèi)容。本文主要記錄和分析日志中的ConcurrentModificationException關(guān)鍵字報警,首先介紹了系統(tǒng)整體架構(gòu)和關(guān)鍵代碼;然后從ArrayList源碼和線程安全兩個方面分析問題產(chǎn)生原因,最后我提出了修復(fù)該問題的方案和類似問題的思考,希望對大家有幫助。

以上就是ConcurrentModificationException日志關(guān)鍵字報警思考分析的詳細(xì)內(nèi)容,更多關(guān)于ConcurrentModificationException報警的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java獲取當(dāng)前時間并轉(zhuǎn)化為yyyy-MM-dd?HH:mm:ss格式的多種方式
這篇文章主要介紹了Java獲取當(dāng)前時間并轉(zhuǎn)化為yyyy-MM-dd?HH:mm:ss格式的多種方式,每種方式結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-03-03
如何解決Mybatis-plus中@TableLogic注解失效問題
這篇文章主要介紹了如何解決Mybatis-plus中@TableLogic注解失效問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
Eclipse+Java+Swing+Mysql實(shí)現(xiàn)工資管理系統(tǒng)
這篇文章主要介紹了Eclipse+Java+Swing+Mysql實(shí)現(xiàn)工資管理系統(tǒng),對正在工作或者學(xué)習(xí)的你有一定的參考價值,需要的朋友可以參考一下2022-01-01
詳解Spring Cloud 跨服務(wù)數(shù)據(jù)聚合框架
這篇文章主要介紹了詳解Spring Cloud 跨服務(wù)數(shù)據(jù)聚合框架,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03

