摘要:中有三個(gè)迭代器相關(guān)的函數(shù),返回兩種迭代器實(shí)現(xiàn),分別是和。根據(jù)堆棧信息找到出錯(cuò)的地方可以看到,保證其遍歷時(shí)不被修改,采用的是用一個(gè)計(jì)數(shù)器的機(jī)制。
今天組內(nèi)的一個(gè)同學(xué)碰到一個(gè)并發(fā)問(wèn)題,幫忙看了下。是個(gè)比較小的點(diǎn),但由于之前沒(méi)碰到過(guò)所以也沒(méi)特意了解過(guò)這塊,今天既然看了就沉淀下來(lái)。
原始問(wèn)題是看到日志里有一些零星的異常,如下如所示
根據(jù)堆棧信息,可以很快定位到對(duì)應(yīng)的應(yīng)用代碼,同時(shí)根據(jù)異常的描述,可以初步定為是并發(fā)訪問(wèn)ArrayList造成的。
相關(guān)應(yīng)用代碼如下(也就是堆棧第三行的CommonUtil.getItemFromList)
這里的list是由上層邏輯傳入的
提到Collection的遍歷,第一時(shí)間想到兩種可能性(非針對(duì)java,只是一般性的想法):
迭代器內(nèi)部會(huì)保存當(dāng)前的遍歷位置,那么多個(gè)線程同時(shí)遍歷時(shí)遍歷位置屬于共享變量,會(huì)導(dǎo)致多線程問(wèn)題
在一個(gè)線程遍歷過(guò)程中,List被其他線程修改,導(dǎo)致List長(zhǎng)度產(chǎn)生變化
多線程遍歷安全對(duì)于以上兩個(gè)可能性,其實(shí)只要稍加思考,就能想到第一個(gè)可能性是不太可能的,因?yàn)槭莏ava基本要保證的。通過(guò)查看ArrayList的源碼也基本確定了這個(gè)點(diǎn)。
ArrayList中有三個(gè)迭代器相關(guān)的函數(shù),返回兩種迭代器實(shí)現(xiàn),分別是ListIterator和Iterator。看名字就知道前者只能用于List的遍歷,后者可用于所有Collection的遍歷,對(duì)于for循環(huán)來(lái)說(shuō),使用的是后者。這點(diǎn)參考這兩個(gè)頁(yè)面。
http://beginnersbook.com/2014...
https://stackoverflow.com/que...
Iterator相關(guān)代碼如下
從這里就可以看出來(lái),多線程遍歷同一個(gè)List是安全的。因?yàn)榈魇窃诿看蝔or循環(huán)(調(diào)用iterator)時(shí)生成的實(shí)例,每次實(shí)例獨(dú)立保存當(dāng)前的遍歷進(jìn)度(圖中的cursor字段),這樣每個(gè)線程在遍歷時(shí)只會(huì)修改自己線程所創(chuàng)建的Itr對(duì)象,沒(méi)有共享變量被修改。
遍歷中修改不安全排除了上面這種可能性,問(wèn)題因?yàn)榛揪投ㄎ涣恕?/p>
根據(jù)堆棧信息找到出錯(cuò)的地方
可以看到,List保證其遍歷時(shí)不被修改,采用的是用一個(gè)計(jì)數(shù)器的機(jī)制。
在開(kāi)始遍歷前,先記錄當(dāng)前的modCount值
而后每次訪問(wèn)下一個(gè)元素之前,都會(huì)檢查下modCount值是否變化,如果有變化,說(shuō)明List的長(zhǎng)度有變化。一旦長(zhǎng)度有變化,就會(huì)拋出ConcurrentModificationException異常。
modCount的注釋詳細(xì)說(shuō)明了這個(gè)字段表明List發(fā)生結(jié)構(gòu)性變化(長(zhǎng)度被修改)的次數(shù),也就是刪除插入等操作時(shí),這個(gè)字段要加一。有興趣的讀者可以自行搜索下ArrayList代碼,看看哪些操作會(huì)引起modCount的變化。
定位罪魁禍?zhǔn)?/b>明確了原因,找具體代碼問(wèn)題的時(shí)候反而有些波折。因?yàn)閺拇a看這個(gè)循環(huán)并沒(méi)有什么特別,同事一直說(shuō)是和反射有關(guān)(反射內(nèi)部有時(shí)候會(huì)對(duì)類的某些字段的可訪問(wèn)標(biāo)進(jìn)行修改),但我自己跟了代碼并沒(méi)有發(fā)現(xiàn)什么可疑的地方,無(wú)奈寫(xiě)了個(gè)小demo驗(yàn)證下。
public class MultiThreadArrayListThread { public static List list = new ArrayList(); public static Random random = new Random(System.currentTimeMillis()); public static class TestBean { private Integer value; public Integer getValue() { return value; } public void setValue(Integer value) { this.value = value; } } public static class TestThread extends Thread { @Override public void run() { for (Object o : list) { /*if (Thread.currentThread().getName().equals("1")) { list.add(new TestBean()); }*/ try { System.out.println(Thread.currentThread().getName() + ":" + org.apache.commons.beanutils.BeanUtils.getProperty(o, "value")); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } try { Thread.sleep(random.nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { int i = 0; while (i < 100) { TestBean testBean = new TestBean(); testBean.setValue(i); list.add(testBean); i++; } int thread = 0; while (thread < 20) { TestThread testThread = new TestThread(); testThread.setName(String.valueOf(thread)); testThread.start(); thread++; } } }
上述代碼執(zhí)行后并沒(méi)有報(bào)錯(cuò),只有在注釋掉的add操作打開(kāi)后,才會(huì)拋異常。
這個(gè)demo進(jìn)一步驗(yàn)證了自己對(duì)于異常原因的認(rèn)知,同時(shí)也說(shuō)明了反射的確不會(huì)影響List的遍歷。因此我的注意力從這段代碼中移開(kāi),轉(zhuǎn)而關(guān)注List的獲取。
這下發(fā)現(xiàn)問(wèn)題所在了。
這里同事犯了個(gè)低級(jí)錯(cuò)誤。這段代碼的邏輯是有ABCD四個(gè)配置信息,要返回這四個(gè)配置信息的并集。但同事的代碼直接在第一個(gè)List中添加后幾個(gè)List的元素了。由于引用是同一個(gè),因此出現(xiàn)了線程a在執(zhí)行完這段邏輯拿到一個(gè)List(其中包含A+B+C+D)并開(kāi)始遍歷時(shí),線程b開(kāi)始執(zhí)行這段邏輯。此時(shí)線程a和線程b拿到的其實(shí)是同一個(gè)List引用(最開(kāi)始的A),并且在線程a遍歷時(shí)線程b對(duì)其進(jìn)行了修改(add(B/C/D)),因此會(huì)觸發(fā)線程a拋異常。不僅如此,哪怕不拋異常,每次業(yè)務(wù)要去拿這個(gè)配置文件,都會(huì)在該集合中加入BCD的元素,集合元素會(huì)遞增(A -> ABCD -> ABCDBCD -> ABCDBCDBCD …),一直運(yùn)行會(huì)導(dǎo)致OOM!
定位到問(wèn)題后修復(fù)就很簡(jiǎn)單了,每次獲取配置時(shí)new一個(gè)新的List即可。
ArrayList list = new ArrayList(); list.add(A); list.add(B); list.add(C); list.add(D);
至此問(wèn)題順利結(jié)局~
小結(jié)這個(gè)問(wèn)題最終定位到是一個(gè)低級(jí)的代碼錯(cuò)誤,但過(guò)程還是值得記錄下的。自己雖在java這方面工作數(shù)年,但像modCount這種機(jī)制,要是沒(méi)有遇到特定的問(wèn)題還是沒(méi)可能面面俱到每個(gè)小點(diǎn)都關(guān)注到的。今天碰到的這個(gè)小case正好幫助自己拾遺補(bǔ)缺,相信以后碰到ArrayList相關(guān)的問(wèn)題,會(huì)更容易解決~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/76299.html
摘要:中有三個(gè)迭代器相關(guān)的函數(shù),返回兩種迭代器實(shí)現(xiàn),分別是和。根據(jù)堆棧信息找到出錯(cuò)的地方可以看到,保證其遍歷時(shí)不被修改,采用的是用一個(gè)計(jì)數(shù)器的機(jī)制。 今天組內(nèi)的一個(gè)同學(xué)碰到一個(gè)并發(fā)問(wèn)題,幫忙看了下。是個(gè)比較小的點(diǎn),但由于之前沒(méi)碰到過(guò)所以也沒(méi)特意了解過(guò)這塊,今天既然看了就沉淀下來(lái)。 原始問(wèn)題是看到日志里有一些零星的異常,如下如所示 showImg(https://segmentfault.co...
摘要:中有三個(gè)迭代器相關(guān)的函數(shù),返回兩種迭代器實(shí)現(xiàn),分別是和。根據(jù)堆棧信息找到出錯(cuò)的地方可以看到,保證其遍歷時(shí)不被修改,采用的是用一個(gè)計(jì)數(shù)器的機(jī)制。 今天組內(nèi)的一個(gè)同學(xué)碰到一個(gè)并發(fā)問(wèn)題,幫忙看了下。是個(gè)比較小的點(diǎn),但由于之前沒(méi)碰到過(guò)所以也沒(méi)特意了解過(guò)這塊,今天既然看了就沉淀下來(lái)。 原始問(wèn)題是看到日志里有一些零星的異常,如下如所示 showImg(https://segmentfault.co...
摘要:復(fù)雜度分析時(shí)間復(fù)雜度遍歷次空間復(fù)雜度還有沒(méi)有優(yōu)化空間方法在某些特定場(chǎng)景下會(huì)進(jìn)行不必要的復(fù)制操作,影響性能。注意尾部的元素有可能是需要剔除的,所以,下一輪循環(huán)要從當(dāng)前索引重新開(kāi)始。 給定一個(gè)數(shù)組 nums?和一個(gè)值 val,你需要原地移除所有數(shù)值等于?val?的元素,返回移除后數(shù)組的新長(zhǎng)度。不要使用額外的數(shù)組空間,你必須在原地修改輸入數(shù)組并在使用 O(1) 額外空間的條件下完成。 元素的...
摘要:告訴當(dāng)前執(zhí)行的線程為線程池中其他具有相同優(yōu)先級(jí)的線程提供機(jī)會(huì)。不能保證會(huì)立即使當(dāng)前正在執(zhí)行的線程處于可運(yùn)行狀態(tài)。當(dāng)達(dá)到超時(shí)時(shí)間時(shí),主線程和是同樣可能的執(zhí)行者候選。下一篇并發(fā)編程線程安全性深層原因 Thread 使用Java的同學(xué)對(duì)Thread應(yīng)該不陌生了,線程的創(chuàng)建和啟動(dòng)等這里就不講了,這篇主要講幾個(gè)容易被忽視的方法以及線程狀態(tài)遷移。 wait/notify/notifyAll 首先我...
閱讀 2239·2021-11-22 13:52
閱讀 3876·2021-11-10 11:36
閱讀 1419·2021-09-24 09:47
閱讀 1096·2019-08-29 13:54
閱讀 3371·2019-08-29 13:46
閱讀 1952·2019-08-29 12:16
閱讀 2120·2019-08-26 13:26
閱讀 3477·2019-08-23 17:10