日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > windows >内容正文

windows

ConcurrentModificationException日志关键字报警引发的思考

發(fā)布時(shí)間:2023/12/24 windows 47 coder
生活随笔 收集整理的這篇文章主要介紹了 ConcurrentModificationException日志关键字报警引发的思考 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文將記錄和分析日志中的ConcurrentModificationException關(guān)鍵字報(bào)警,還有一些我的思考,希望對(duì)大家有幫助。

一、背景

近期,在日常的日志關(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è)工具類(lèi),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)聽(tīng)消息進(jìn)行消費(fèi)。在消費(fèi)邏輯中,會(huì)調(diào)用MyRpcServicegetList()?方法進(jìn)行狀態(tài)查詢,因?yàn)椴樵兊臓顟B(tài)是固定的,所以在Consumer類(lèi)中定義了static?final?類(lèi)型的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)鍵代碼,不知道你有沒(méi)有發(fā)現(xiàn)問(wèn)題?可以先拋開(kāi)設(shè)計(jì)和代碼實(shí)現(xiàn)方面的問(wèn)題不談,只看這樣的代碼能不能正常執(zhí)行,得到正確的業(yè)務(wù)結(jié)果。

既然這么問(wèn)了,當(dāng)然會(huì)有問(wèn)題:在高并發(fā)環(huán)境下,MQ應(yīng)用在消費(fèi)消息時(shí),調(diào)用RPC服務(wù)查詢時(shí)可能會(huì)拋出異常,從而觸發(fā)MQ異常重試,至于對(duì)業(yè)務(wù)有沒(méi)有影響,得具體問(wèn)題具體分析了。

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?分析1-ArrayList源碼

從日志中可以看到,ConcurrentModificationExceptionjava.util.ArrayList類(lèi)里面的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ù)不一致問(wèn)題。

再可以看下源碼中modCount的注釋?zhuān)笠馐牵?/p>

modCount表示ArrayList自從創(chuàng)建以來(lái)結(jié)構(gòu)上發(fā)生的修改次數(shù)。結(jié)構(gòu)修改是指改變列表大小的修改,或者以其他方式擾亂列表,使正在進(jìn)行的迭代可能產(chǎn)生不正確的結(jié)果。

modCount字段用于iteratorlistIterator方法返回的迭代器(或列表迭代器)。如果這個(gè)字段的值在迭代過(guò)程中發(fā)生意外的變化,迭代器(或列表迭代器)將在next、remove、previous、set或add操作時(shí)拋出ConcurrentModificationException異常。這提供了fail-fast(快速失?。┬袨椋皇窃诘^(guò)程中遇到并發(fā)修改時(shí)具有不確定性。

子類(lèi)可以選擇使用這個(gè)字段。如果子類(lèi)希望提供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?分析2-線程安全問(wèn)題

有個(gè)有趣的現(xiàn)象是,這個(gè)異常日志僅存在MQ應(yīng)用中,這是為什么呢?

這其實(shí)是一個(gè)多線程問(wèn)題。我們知道,static對(duì)象是在類(lèi)加載時(shí)創(chuàng)建的全局對(duì)象,它們的生命周期與類(lèi)的生命周期相同。static對(duì)象在程序啟動(dòng)時(shí)創(chuàng)建,在程序結(jié)束時(shí)銷(xiāo)毀。這意味著static對(duì)象在多個(gè)線程之間共享的,可能存在線程安全問(wèn)題。

翻回去仔細(xì)看下代碼,可以看到MyMqConsumer定義的stateList是static類(lèi)型的,是否是否存在線程安全問(wèn)題呢?

在流量較低的情況下,多個(gè)消息不在同一時(shí)刻到達(dá),每個(gè)線程處理消息將不會(huì)爭(zhēng)奪static對(duì)象,所以不會(huì)有問(wèn)題;

當(dāng)流量較大情況下,有多個(gè)消息可能在同一時(shí)刻到達(dá),每個(gè)線程處理過(guò)程中都會(huì)對(duì)stateList進(jìn)行賦值,調(diào)用遠(yuǎn)程RPC接口,它們之間將會(huì)爭(zhēng)奪static對(duì)象,可能存在問(wèn)題。例如上圖中右半部分,線程1還沒(méi)有處理完消息1時(shí),線程2就開(kāi)始爭(zhēng)搶?zhuān)敲淳涂赡苁笰rrayList中modCount?!=?expectedModCount條件滿足,從而拋出異常。

三、改進(jìn)思考

3.1?本問(wèn)題的優(yōu)化

經(jīng)過(guò)上述分析,已經(jīng)清楚問(wèn)題的產(chǎn)生原因了。對(duì)于本問(wèn)題的優(yōu)化,其實(shí)也比較簡(jiǎn)單。有如下兩種方式可供選擇:

1.? 在MyMqConsumer調(diào)用RPC查詢的入?yún)?,使?strong>new?List來(lái)替代原來(lái)的類(lèi)中定義好的static對(duì)象;

2.? 修改KeyUtil代碼,淺拷貝傳入的itemList,再進(jìn)行排序

3.2?類(lèi)似問(wèn)題的發(fā)現(xiàn)和改進(jìn)

本問(wèn)題已經(jīng)修復(fù),那類(lèi)似的問(wèn)題是否可以避免或者減少,將是接下來(lái)值得思考的一個(gè)問(wèn)題。為了減少這類(lèi)問(wèn)題發(fā)生,我結(jié)合平時(shí)工作過(guò)程中的幾個(gè)階段,認(rèn)為可以從以下幾個(gè)方面進(jìn)行改進(jìn):

  • 開(kāi)發(fā)

開(kāi)發(fā)過(guò)程中,開(kāi)發(fā)人員需要提升認(rèn)知和水平,注意代碼中可能存在的線程問(wèn)題;注意編寫(xiě)單元測(cè)試,可以通過(guò)模擬多線程環(huán)境來(lái)檢測(cè)潛在的問(wèn)題。

  • 代碼評(píng)審

開(kāi)發(fā)完成的代碼一定需要進(jìn)行代碼評(píng)審,評(píng)審過(guò)程中架構(gòu)師需要發(fā)揮自己豐富的開(kāi)發(fā)經(jīng)驗(yàn)和較強(qiáng)的代碼直覺(jué),“火眼金睛”,發(fā)現(xiàn)代碼中的漏洞;當(dāng)然這對(duì)評(píng)審人員的要求很高,因?yàn)閮H通過(guò)改動(dòng)的幾行代碼發(fā)現(xiàn)問(wèn)題確實(shí)是一件很有挑戰(zhàn)的事情。如果要有一些自動(dòng)化工具或者插件,則可以起到事半功倍的效果。這里其實(shí)我還沒(méi)有調(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)模塊之間的交互問(wèn)題。除了繼承測(cè)試,有時(shí)還需要性能測(cè)試,性能測(cè)試可以發(fā)現(xiàn)潛在的競(jìng)爭(zhēng)條件、死鎖、資源爭(zhēng)用等多線程問(wèn)題。

四、小結(jié)

最后,我簡(jiǎn)單總結(jié)一下本文內(nèi)容。本文主要記錄和分析日志中的ConcurrentModificationException關(guān)鍵字報(bào)警,首先介紹了系統(tǒng)整體架構(gòu)和關(guān)鍵代碼;然后從ArrayList源碼和線程安全兩個(gè)方面分析問(wèn)題產(chǎn)生原因,最后我提出了修復(fù)該問(wèn)題的方案和類(lèi)似問(wèn)題的思考,希望對(duì)大家有幫助。

總結(jié)

以上是生活随笔為你收集整理的ConcurrentModificationException日志关键字报警引发的思考的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。