ConcurrentModificationException日志关键字报警引发的思考
本文將記錄和分析日志中的ConcurrentModificationException關鍵字報警,還有一些我的思考,希望對大家有幫助。
一、背景
近期,在日常的日志關鍵字報警分析時,發現我負責的一個電商核心系統在某時段存在較多ConcurrentModificationException異常日志,遂進行分析和改進,下面是我的一些思考。
1.1?系統架構
一直以來,無狀態的服務都被當作分布式服務設計的最佳實踐。因為無狀態的服務對于擴展性和運維方面有著得天獨厚的優勢,可以隨意地增加和減少節點。本系統的整體架構可以認為是由一個MQ應用、一個RPC應用和底層存儲組成。
RPC應用是無狀態服務,對外提供常用的查詢和操作接口;采用雙機房部署,每個機房10*8C16G;
MQ應用是無狀態服務,負責消費MQ消息,在消費過程中會調用該RPC應用提供方法;采用雙機房部署,每個機房5*8C16G;
底層存儲用的是數據庫集群和緩存集群,大概如圖所示:
1.2?關鍵代碼
MyRpcService 對外提供RPC服務,getList 方法可以根據入參中的狀態進行查詢,由于業務需要,需要對入參的狀態進行排序,實現部分關鍵代碼如下:
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 方法對入參的itemList進行排序使用的是Java集合框架內置的sort?方法,代碼如下:
public class?KeyUtil?{
public static?String?getKeyString(List<Integer>?itemList) {
????????String?result?= "";
//省略代碼
????????Collections.sort(itemList);
//省略代碼
return?result;
}
}
MyMqConsumer是MQ消費者,負責監聽消息進行消費。在消費邏輯中,會調用MyRpcService的getList()?方法進行狀態查詢,因為查詢的狀態是固定的,所以在Consumer類中定義了static?final?類型的stateList?,關鍵代碼如下:
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);
//?省略其他代碼
}
}
}
二、? 原因分析
看了上面的系統架構和關鍵代碼,不知道你有沒有發現問題?可以先拋開設計和代碼實現方面的問題不談,只看這樣的代碼能不能正常執行,得到正確的業務結果。
既然這么問了,當然會有問題:在高并發環境下,MQ應用在消費消息時,調用RPC服務查詢時可能會拋出異常,從而觸發MQ異常重試,至于對業務有沒有影響,得具體問題具體分析了。
ERROR?執行流程時出錯
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?分析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();
}
}
在該方法中,內部會維護一個expectedModCount變量,賦值為modCount,在每次迭代過程中,迭代器會檢查expectedModCount是否等于當前的modCount。如果不等,說明在迭代過程中ArrayList的結構發生了修改,迭代器會拋出ConcurrentModificationException異常。這種設計可以確保在多線程環境下,當一個線程修改ArrayList時,其他線程在迭代過程中可以立即發現這種修改,從而避免潛在的數據不一致問題。
再可以看下源碼中modCount的注釋,大意是:
modCount表示ArrayList自從創建以來結構上發生的修改次數。結構修改是指改變列表大小的修改,或者以其他方式擾亂列表,使正在進行的迭代可能產生不正確的結果。
modCount字段用于iterator和listIterator方法返回的迭代器(或列表迭代器)。如果這個字段的值在迭代過程中發生意外的變化,迭代器(或列表迭代器)將在next、remove、previous、set或add操作時拋出ConcurrentModificationException異常。這提供了fail-fast(快速失敗)行為,而不是在迭代過程中遇到并發修改時具有不確定性。
子類可以選擇使用這個字段。如果子類希望提供fail-fast迭代器(和列表迭代器),那么它只需在其add(int,?E)和remove(int)方法(以及覆蓋的任何其他導致列表結構修改的方法)中遞增此字段。單次調用add(int,?E)或remove(int)應該在此字段上增加不超過1次,否則迭代器(和列表迭代器)將拋出虛假的ConcurrentModificationException。如果實現不希望提供fail-fast迭代器,可以忽略此字段。
2.2?分析2-線程安全問題
有個有趣的現象是,這個異常日志僅存在MQ應用中,這是為什么呢?
這其實是一個多線程問題。我們知道,static對象是在類加載時創建的全局對象,它們的生命周期與類的生命周期相同。static對象在程序啟動時創建,在程序結束時銷毀。這意味著static對象在多個線程之間共享的,可能存在線程安全問題。
翻回去仔細看下代碼,可以看到MyMqConsumer定義的stateList是static類型的,是否是否存在線程安全問題呢?
在流量較低的情況下,多個消息不在同一時刻到達,每個線程處理消息將不會爭奪static對象,所以不會有問題;
當流量較大情況下,有多個消息可能在同一時刻到達,每個線程處理過程中都會對stateList進行賦值,調用遠程RPC接口,它們之間將會爭奪static對象,可能存在問題。例如上圖中右半部分,線程1還沒有處理完消息1時,線程2就開始爭搶,那么就可能使ArrayList中modCount?!=?expectedModCount條件滿足,從而拋出異常。
三、改進思考
3.1?本問題的優化
經過上述分析,已經清楚問題的產生原因了。對于本問題的優化,其實也比較簡單。有如下兩種方式可供選擇:
1.? 在MyMqConsumer調用RPC查詢的入參,使用new?List來替代原來的類中定義好的static對象;
2.? 修改KeyUtil代碼,淺拷貝傳入的itemList,再進行排序
3.2?類似問題的發現和改進
本問題已經修復,那類似的問題是否可以避免或者減少,將是接下來值得思考的一個問題。為了減少這類問題發生,我結合平時工作過程中的幾個階段,認為可以從以下幾個方面進行改進:
- 開發
開發過程中,開發人員需要提升認知和水平,注意代碼中可能存在的線程問題;注意編寫單元測試,可以通過模擬多線程環境來檢測潛在的問題。
- 代碼評審
開發完成的代碼一定需要進行代碼評審,評審過程中架構師需要發揮自己豐富的開發經驗和較強的代碼直覺,“火眼金睛”,發現代碼中的漏洞;當然這對評審人員的要求很高,因為僅通過改動的幾行代碼發現問題確實是一件很有挑戰的事情。如果要有一些自動化工具或者插件,則可以起到事半功倍的效果。這里其實我還沒有調研相關的工具,如果有大佬有相關經驗歡迎評論交流。
- 測試
測試階段除了驗證正常的業務功能,還需要進行集成測試和性能測試。在集成測試中,將多個模塊組合在一起,測試整個系統在多線程環境中的行為,有助于發現模塊之間的交互問題。除了繼承測試,有時還需要性能測試,性能測試可以發現潛在的競爭條件、死鎖、資源爭用等多線程問題。
四、小結
最后,我簡單總結一下本文內容。本文主要記錄和分析日志中的ConcurrentModificationException關鍵字報警,首先介紹了系統整體架構和關鍵代碼;然后從ArrayList源碼和線程安全兩個方面分析問題產生原因,最后我提出了修復該問題的方案和類似問題的思考,希望對大家有幫助。
總結
以上是生活随笔為你收集整理的ConcurrentModificationException日志关键字报警引发的思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 荣耀 90 / Pro 系列手机支持全新
- 下一篇: Win10新版本又现新广告!微软继续力推