摘要:中有三個迭代器相關的函數,返回兩種迭代器實現,分別是和。根據堆棧信息找到出錯的地方可以看到,保證其遍歷時不被修改,采用的是用一個計數器的機制。
今天組內的一個同學碰到一個并發問題,幫忙看了下。是個比較小的點,但由于之前沒碰到過所以也沒特意了解過這塊,今天既然看了就沉淀下來。
原始問題是看到日志里有一些零星的異常,如下如所示
根據堆棧信息,可以很快定位到對應的應用代碼,同時根據異常的描述,可以初步定為是并發訪問ArrayList造成的。
相關應用代碼如下(也就是堆棧第三行的CommonUtil.getItemFromList)
這里的list是由上層邏輯傳入的
提到Collection的遍歷,第一時間想到兩種可能性(非針對java,只是一般性的想法):
迭代器內部會保存當前的遍歷位置,那么多個線程同時遍歷時遍歷位置屬于共享變量,會導致多線程問題
在一個線程遍歷過程中,List被其他線程修改,導致List長度產生變化
多線程遍歷安全對于以上兩個可能性,其實只要稍加思考,就能想到第一個可能性是不太可能的,因為是java基本要保證的。通過查看ArrayList的源碼也基本確定了這個點。
ArrayList中有三個迭代器相關的函數,返回兩種迭代器實現,分別是ListIterator和Iterator。看名字就知道前者只能用于List的遍歷,后者可用于所有Collection的遍歷,對于for循環來說,使用的是后者。這點參考這兩個頁面。
http://beginnersbook.com/2014...
https://stackoverflow.com/que...
Iterator相關代碼如下
從這里就可以看出來,多線程遍歷同一個List是安全的。因為迭代器是在每次for循環(調用iterator)時生成的實例,每次實例獨立保存當前的遍歷進度(圖中的cursor字段),這樣每個線程在遍歷時只會修改自己線程所創建的Itr對象,沒有共享變量被修改。
遍歷中修改不安全排除了上面這種可能性,問題因為基本就定位了。
根據堆棧信息找到出錯的地方
可以看到,List保證其遍歷時不被修改,采用的是用一個計數器的機制。
在開始遍歷前,先記錄當前的modCount值
而后每次訪問下一個元素之前,都會檢查下modCount值是否變化,如果有變化,說明List的長度有變化。一旦長度有變化,就會拋出ConcurrentModificationException異常。
modCount的注釋詳細說明了這個字段表明List發生結構性變化(長度被修改)的次數,也就是刪除插入等操作時,這個字段要加一。有興趣的讀者可以自行搜索下ArrayList代碼,看看哪些操作會引起modCount的變化。
定位罪魁禍首明確了原因,找具體代碼問題的時候反而有些波折。因為從代碼看這個循環并沒有什么特別,同事一直說是和反射有關(反射內部有時候會對類的某些字段的可訪問標進行修改),但我自己跟了代碼并沒有發現什么可疑的地方,無奈寫了個小demo驗證下。
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++; } } }
上述代碼執行后并沒有報錯,只有在注釋掉的add操作打開后,才會拋異常。
這個demo進一步驗證了自己對于異常原因的認知,同時也說明了反射的確不會影響List的遍歷。因此我的注意力從這段代碼中移開,轉而關注List的獲取。
這下發現問題所在了。
這里同事犯了個低級錯誤。這段代碼的邏輯是有ABCD四個配置信息,要返回這四個配置信息的并集。但同事的代碼直接在第一個List中添加后幾個List的元素了。由于引用是同一個,因此出現了線程a在執行完這段邏輯拿到一個List(其中包含A+B+C+D)并開始遍歷時,線程b開始執行這段邏輯。此時線程a和線程b拿到的其實是同一個List引用(最開始的A),并且在線程a遍歷時線程b對其進行了修改(add(B/C/D)),因此會觸發線程a拋異常。不僅如此,哪怕不拋異常,每次業務要去拿這個配置文件,都會在該集合中加入BCD的元素,集合元素會遞增(A -> ABCD -> ABCDBCD -> ABCDBCDBCD …),一直運行會導致OOM!
定位到問題后修復就很簡單了,每次獲取配置時new一個新的List即可。
ArrayList list = new ArrayList(); list.add(A); list.add(B); list.add(C); list.add(D);
至此問題順利結局~
小結這個問題最終定位到是一個低級的代碼錯誤,但過程還是值得記錄下的。自己雖在java這方面工作數年,但像modCount這種機制,要是沒有遇到特定的問題還是沒可能面面俱到每個小點都關注到的。今天碰到的這個小case正好幫助自己拾遺補缺,相信以后碰到ArrayList相關的問題,會更容易解決~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76299.html
摘要:中有三個迭代器相關的函數,返回兩種迭代器實現,分別是和。根據堆棧信息找到出錯的地方可以看到,保證其遍歷時不被修改,采用的是用一個計數器的機制。 今天組內的一個同學碰到一個并發問題,幫忙看了下。是個比較小的點,但由于之前沒碰到過所以也沒特意了解過這塊,今天既然看了就沉淀下來。 原始問題是看到日志里有一些零星的異常,如下如所示 showImg(https://segmentfault.co...
摘要:中有三個迭代器相關的函數,返回兩種迭代器實現,分別是和。根據堆棧信息找到出錯的地方可以看到,保證其遍歷時不被修改,采用的是用一個計數器的機制。 今天組內的一個同學碰到一個并發問題,幫忙看了下。是個比較小的點,但由于之前沒碰到過所以也沒特意了解過這塊,今天既然看了就沉淀下來。 原始問題是看到日志里有一些零星的異常,如下如所示 showImg(https://segmentfault.co...
摘要:復雜度分析時間復雜度遍歷次空間復雜度還有沒有優化空間方法在某些特定場景下會進行不必要的復制操作,影響性能。注意尾部的元素有可能是需要剔除的,所以,下一輪循環要從當前索引重新開始。 給定一個數組 nums?和一個值 val,你需要原地移除所有數值等于?val?的元素,返回移除后數組的新長度。不要使用額外的數組空間,你必須在原地修改輸入數組并在使用 O(1) 額外空間的條件下完成。 元素的...
摘要:告訴當前執行的線程為線程池中其他具有相同優先級的線程提供機會。不能保證會立即使當前正在執行的線程處于可運行狀態。當達到超時時間時,主線程和是同樣可能的執行者候選。下一篇并發編程線程安全性深層原因 Thread 使用Java的同學對Thread應該不陌生了,線程的創建和啟動等這里就不講了,這篇主要講幾個容易被忽視的方法以及線程狀態遷移。 wait/notify/notifyAll 首先我...
閱讀 2212·2021-11-22 13:52
閱讀 3846·2021-11-10 11:36
閱讀 1380·2021-09-24 09:47
閱讀 1088·2019-08-29 13:54
閱讀 3360·2019-08-29 13:46
閱讀 1942·2019-08-29 12:16
閱讀 2108·2019-08-26 13:26
閱讀 3471·2019-08-23 17:10