程式碼中對同一個list進行操作增加刪除的操作,有多種情況,請根據需要選擇,否則入坑
轉載:https://blog.csdn.net/lu1024188315/article/details/74082227
環境:JDK 1.8.0_111
在Java開發過程中,使用iterator遍歷集合的同時對集合進行修改就會出現java.util.ConcurrentModificationException異常,本文就以ArrayList為例去理解和解決這種異常。
一、單執行緒情況下問題分析及解決方案
1.1 問題復現
先上一段拋異常的程式碼。
1 public void test1() { 2 ArrayList<Integer> arrayList = new ArrayList<>();3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 // 復現方法一 8 Iterator<Integer> iterator = arrayList.iterator(); 9 while (iterator.hasNext()) { 10 Integer integer = iterator.next(); 11 if(integer.intValue() == 5) { 12 arrayList.remove(integer); 13 } 14 } 15 16 // 復現方法二 17 iterator = arrayList.iterator(); 18 for (Integer value : arrayList) { 19 Integer integer = iterator.next(); 20 if (integer.intValue() == 5) {21 arrayList.remove(integer); 22 } 23 } 24 }
在這個程式碼中展示了兩種能拋異常的實現方式。
1.2、問題原因分析
先來看實現方法一,方法一中使用Iterator遍歷ArrayList, 丟擲異常的是iterator.next()。看下Iterator next方法實現原始碼
1 public E next() { 2 checkForComodification(); 3 int i = cursor; 4 if (i >= size) 5 throw new NoSuchElementException(); 6 Object[] elementData = ArrayList.this.elementData; 7 if (i >= elementData.length) 8 throw new ConcurrentModificationException(); 9 cursor = i + 1; 10 return (E) elementData[lastRet = i]; 11 } 12 13 final void checkForComodification() { 14 if (modCount != expectedModCount) 15 throw new ConcurrentModificationException(); 16 }
在next方法中首先呼叫了checkForComodification方法,該方法會判斷modCount是否等於expectedModCount,不等於就會丟擲java.util.ConcurrentModificationExcepiton異常。
我們接下來跟蹤看一下modCount和expectedModCount的賦值和修改。
modCount是ArrayList的一個屬性,繼承自抽象類AbstractList,用於表示ArrayList物件被修改次數。
1 protected transient int modCount = 0;
整個ArrayList中修改modCount的方法比較多,有add、remove、clear、ensureCapacityInternal等,凡是設計到ArrayList物件修改的都會自增modCount屬性。
在建立Iterator的時候會將modCount賦值給expectedModCount,在遍歷ArrayList過程中,沒有其他地方可以設定expectedModCount了,因此遍歷過程中expectedModCount會一直保持初始值20(呼叫add方法添加了20個元素,修改了20次)。
1 int expectedModCount = modCount; // 建立物件時初始化
遍歷的時候是不會觸發modCount自增的,但是遍歷到integer.intValue() == 5的時候,執行了一次arrayList.remove(integer),這行程式碼執行後modCount++變為了21,但此時的expectedModCount仍然為20。
1 final void checkForComodification() { 2 if (modCount != expectedModCount) 3 throw new ConcurrentModificationException(); 4 }
在執行next方法時,遇到modCount != expectedModCount方法,導致丟擲異常java.util.ConcurrentModificationException。
明白了丟擲異常的過程,但是為什麼要這麼做呢?很明顯這麼做是為了阻止程式設計師在不允許修改的時候修改物件,起到保護作用,避免出現未知異常。引用網上的一段解釋,點選檢視解釋來源
Iterator 是工作在一個獨立的執行緒中,並且擁有一個 mutex 鎖。 Iterator 被建立之後會建立一個指向原來物件的單鏈索引表,當原來的物件數量發生變化時,這個索引表的內容不會同步改變。 當索引指標往後移動的時候就找不到要迭代的物件,所以按照 fail-fast 原則 Iterator 會馬上丟擲 java.util.ConcurrentModificationException 異常。 所以 Iterator 在工作的時候是不允許被迭代的物件被改變的。但你可以使用 Iterator 本身的方法 remove() 來刪除物件, Iterator.remove() 方法會在刪除當前迭代物件的同時維護索引的一致性。
再來分析下第二種for迴圈拋異常的原因:
1 public void forEach(Consumer<? super E> action) { 2 Objects.requireNonNull(action); 3 final int expectedModCount = modCount; 4 @SuppressWarnings("unchecked") 5 final E[] elementData = (E[]) this.elementData; 6 final int size = this.size; 7 for (int i=0; modCount == expectedModCount && i < size; i++) { 8 action.accept(elementData[i]); 9 } 10 if (modCount != expectedModCount) { 11 throw new ConcurrentModificationException(); 12 } 13 }
在for迴圈中一開始也是對expectedModCount採用modCount進行賦值。在進行for迴圈時每次都會有判定條件modCount == expectedModCount,當執行完arrayList.remove(integer)之後,該判定條件返回false退出迴圈,然後執行if語句,結果同樣丟擲java.util.ConcurrentModificationException異常。
這兩種復現方法實際上都是同一個原因導致的。
1.3 問題解決方案
上述的兩種復現方法都是在單執行緒執行的,先來說明單執行緒中的解決方案:
1 public void test2() { 2 ArrayList<Integer> arrayList = new ArrayList<>(); 3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 Iterator<Integer> iterator = arrayList.iterator(); 8 while (iterator.hasNext()) { 9 Integer integer = iterator.next(); 10 if (integer.intValue() == 5) { 11 iterator.remove(); 12 } 13 } 14 }
這種解決方案最核心的就是呼叫iterator.remove()方法。我們看看該方法原始碼為什麼這個方法能避免丟擲異常
1 public void remove() { 2 if (lastRet < 0) 3 throw new IllegalStateException(); 4 checkForComodification(); 5 6 try { 7 ArrayList.this.remove(lastRet); 8 cursor = lastRet; 9 lastRet = -1; 10 expectedModCount = modCount; 11 } catch (IndexOutOfBoundsException ex) { 12 throw new ConcurrentModificationException(); 13 } 14 }
在iterator.remove()方法中,同樣呼叫了ArrayList自身的remove方法,但是呼叫完之後並非就return了,而是expectedModCount = modCount重置了expectedModCount值,使二者的值繼續保持相等。
針對forEach迴圈並沒有修復方案,因此在遍歷過程中同時需要修改ArrayList物件,則需要採用iterator遍歷。
上面提出的解決方案呼叫的是iterator.remove()方法,如果不僅僅是想呼叫remove方法移除元素,還想增加元素,或者替換元素,是否可以呢?瀏覽Iterator原始碼可以發現這是不行的,Iterator只提供了remove方法。
但是ArrayList實現了ListIterator介面,ListIterator類繼承了Iter,這些操作都是可以實現的,使用示例如下:
1 public void test3() { 2 ArrayList<Integer> arrayList = new ArrayList<>(); 3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 ListIterator<Integer> iterator = arrayList.listIterator(); 8 while (iterator.hasNext()) { 9 Integer integer = iterator.next(); 10 if (integer.intValue() == 5) { 11 iterator.set(Integer.valueOf(6)); 12 iterator.remove(); 13 iterator.add(integer); 14 } 15 } 16 }
二、 多執行緒情況下的問題分析及解決方案
單執行緒問題解決了,再來看看多執行緒情況。
2.1 問題復現
1 public void test4() { 2 ArrayList<Integer> arrayList = new ArrayList<>(); 3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 Thread thread1 = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 ListIterator<Integer> iterator = arrayList.listIterator(); 11 while (iterator.hasNext()) { 12 System.out.println("thread1 " + iterator.next().intValue()); 13 try { 14 Thread.sleep(1000); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 } 19 } 20 }); 21 22 Thread thread2 = new Thread(new Runnable() { 23 @Override 24 public void run() { 25 ListIterator<Integer> iterator = arrayList.listIterator(); 26 while (iterator.hasNext()) { 27 System.out.println("thread2 " + iterator.next().intValue()); 28 iterator.remove(); 29 } 30 } 31 }); 32 thread1.start(); 33 thread2.start(); 34 }
在個測試程式碼中,開啟兩個執行緒,一個執行緒遍歷,另外一個執行緒遍歷加修改。程式輸出結果如下
thread1 0 thread2 0 thread2 1 thread2 2 thread2 3 thread2 4 thread2 5 thread2 6 thread2 7 thread2 8 thread2 9 thread2 10 thread2 11 thread2 12 thread2 13 thread2 14 thread2 15 thread2 16 thread2 17 thread2 18 thread2 19 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.snow.ExceptionTest$1.run(ExceptionTest.java:74) at java.lang.Thread.run(Thread.java:745) Process finished with exit code 0
2.2 問題分析
從上面程式碼執行結果可以看出thread2 遍歷結束後,thread1 sleep完1000ms準備遍歷第二個元素,next的時候丟擲異常了。我們從時間點分析一下拋異常的原因
時間點 | arrayList.modCount | thread1 iterator.expectedModCount | thread2 iterator.expectedModCount |
thread start,初始化iterator | 20 | 20 | 20 |
thread2.remove()呼叫之後 | 21 | 20 | 21 |
兩個thread都是使用的同一個arrayList,thread2修改完後modCount = 21,此時thread2的expectedModCount = 21 可以一直遍歷到結束;thread1的expectedModCount仍然為20,因為thread1的expectedModCount只是在初始化的時候賦值,其後並未被修改過。因此當arrayList的modCount被thread2修改為21之後,thread1想繼續遍歷必定會丟擲異常了。
在這個示例程式碼裡面,兩個thread,每個thread都有自己的iterator,當thread2通過iterator方法修改expectedModCount必定不會被thread1感知到。這個跟ArrayList非執行緒安全是無關的,即使這裡面的ArrayList換成Vector也是一樣的結果,不信上測試程式碼:
1 public void test5() { 2 Vector<Integer> vector = new Vector<>(); 3 for (int i = 0; i < 20; i++) { 4 vector.add(Integer.valueOf(i)); 5 } 6 7 Thread thread1 = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 ListIterator<Integer> iterator = vector.listIterator(); 11 while (iterator.hasNext()) { 12 System.out.println("thread1 " + iterator.next().intValue()); 13 try { 14 Thread.sleep(1000); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 } 19 } 20 }); 21 22 Thread thread2 = new Thread(new Runnable() { 23 @Override 24 public void run() { 25 ListIterator<Integer> iterator = vector.listIterator(); 26 while (iterator.hasNext()) { 27 Integer integer = iterator.next(); 28 System.out.println("thread2 " + integer.intValue()); 29 if (integer.intValue() == 5) { 30 iterator.remove(); 31 } 32 } 33 } 34 }); 35 thread1.start(); 36 thread2.start(); 37 }
執行後輸出結果為:
thread1 0 thread2 0 thread2 1 thread2 2 thread2 3 thread2 4 thread2 5 thread2 6 thread2 7 thread2 8 thread2 9 thread2 10 thread2 11 thread2 12 thread2 13 thread2 14 thread2 15 thread2 16 thread2 17 thread2 18 thread2 19 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.Vector$Itr.checkForComodification(Vector.java:1184) at java.util.Vector$Itr.next(Vector.java:1137) at com.snow.ExceptionTest$3.run(ExceptionTest.java:112) at java.lang.Thread.run(Thread.java:745) Process finished with exit code 0
test5()方法執行結果和test4()是相同的,那如何解決這個問題呢?
2.3 多執行緒下的解決方案
2.3.1 方案一:iterator遍歷過程加同步鎖,鎖住整個arrayList
1 public static void test5() { 2 ArrayList<Integer> arrayList = new ArrayList<>(); 3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 Thread thread1 = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 synchronized (arrayList) { 11 ListIterator<Integer> iterator = arrayList.listIterator(); 12 while (iterator.hasNext()) { 13 System.out.println("thread1 " + iterator.next().intValue()); 14 try { 15 Thread.sleep(100); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 } 20 } 21 } 22 }); 23 24 Thread thread2 = new Thread(new Runnable() { 25 @Override 26 public void run() { 27 synchronized (arrayList) { 28 ListIterator<Integer> iterator = arrayList.listIterator(); 29 while (iterator.hasNext()) { 30 Integer integer = iterator.next(); 31 System.out.println("thread2 " + integer.intValue()); 32 if (integer.intValue() == 5) { 33 iterator.remove(); 34 } 35 } 36 } 37 } 38 }); 39 thread1.start(); 40 thread2.start(); 41 }
這種方案本質上是將多執行緒通過加鎖來轉變為單執行緒操作,確保同一時間內只有一個執行緒去使用iterator遍歷arrayList,其它執行緒等待,效率顯然是隻有單執行緒的效率。
2.3.2 方案二:使用CopyOnWriteArrayList,有坑!要明白原理再用,否則你就呆坑裡吧。
相關推薦
程式碼中對同一個list進行操作增加刪除的操作,有多種情況,請根據需要選擇,否則入坑
轉載:https://blog.csdn.net/lu1024188315/article/details/74082227環境:JDK 1.8.0_111在Java開發過程中,使用iterator遍歷集合的同時對集合進行修改就會出現java.util.ConcurrentM
QT——如何在不同執行緒中對同一個UI介面進行操作
最近在做一個介面,這個介面的功能有兩個: (1)點選開始按鈕,進入迴圈,等待裝置插入; (2)點選停止按鈕,中止等待過程。 對於“開始”按鈕,很自然的就寫了個while,在迴圈裡等待裝置插入。但是這就出現一個問題:這個執行緒就直接陷進了while裡,就是說點選“取消”沒
C#中對 XML節點進行新增,刪除,查詢和刪除操作
從網上整理所得 XMLDocument來操作XML比較簡單,雖然有時效率不是很高。程式碼如下 已知有一個XML檔案(bookstore.xml)如下: <?xml version="1.0" encoding="gb2312"?> <books
Delphi中使用cxGrid對資料集進行Sort和Locate操作
核心提示:在編寫某個系統時,由於使用了資料集型別無關技術(即資料集可能是ADOQuery,也有可能是TClientDataSet等等)。當需要對資料進行排序和查詢時,只好利用cxGrid自身的功能來實現:fun... 在編寫某個系統時,由於使用了資料集型別無關技術(即資料集可能是ADOQuery,也有可
關於java程式碼中對類進行重定義的探究(即匿名內部類)。包含欄位定義,方法重寫,追加方法
寫這篇部落格的時候是在看java8實戰,其中發現了一段程式碼,感覺很奇怪,之前雖然接觸過方法的動態重寫,但是沒見過此種程式碼 大致長這樣: 問題的關鍵不在於這是一個lambda表示式,而是後面大括號內直接追加程式碼,最後實現的效果跟你新建一個HashMap,然後在呼叫兩次
mysql 對同一個表進行更新操作
update ea_account ea join ( select id from ea_account where trader_id='99' ) as t set account='22' where ea.id=t.id
Java中對String字符串的常用操作
with 3.4 () val pareto exc case byte ring 這周遇到了一個需要處理String字符串的問題,用到了split將字符串解析為一個String的數組,還用到了某些替換字符的操作。 1 /* 2 **將String source按‘,
Django數據查詢中對字段進行排序
www. .cn href ref 倒序 pan 兩個 ctime bject 第一種方法:使用order_by進行排序 Articlelist = Article.objects.filter(**kwargs).order_by(‘nid‘) Articlelist =
Linux中對邏輯卷進行擴容
文章 建立 文件 linu 使用 url xtend 位置 5G 一、在擴容之前,先查看自己邏輯卷,卷組,物理卷的信息:(在上一篇的基礎上:Linux中對邏輯卷的建立) 查看物理卷: # pvdisplay /dev/sdc1 查看卷組: vgdisplay /de
怎麽在iPhone手機中對CAD圖紙進行查看?
發展 png 最新版本 term 想要 mage 查找 好的 就會 怎麽在iPhone手機中對CAD圖紙進行查看?現在隨著科技的不斷發展,我們不僅僅只局限於在電腦中進行日常的工作,現在移動端手機也可以進行工作,如果剛好你需要外出,但在這個時候公司的老板給你打電話有一張緊急的
怎麼在手機中對CAD圖紙進行圖層和佈局管理?
怎麼在手機中對CAD圖紙進行圖層和佈局管理?現在隨著科技的發展,對我們的生活和工作都有了很大的變化,現在手機不僅僅只是一個通話工具了,現在它有了更大的作用,就是在手機中也能輕輕鬆鬆的完成工作,那就是可以在手機中對CAD圖紙進行管理,以前只能在電腦中完成,現在移動端也可以實現。但是怎麼在手機中對CAD圖紙進行圖
在C#中對枚舉進行位運算--枚舉組合
code 實例方法 類型 public 最大 數值 必須 span bsp 由於枚舉的基礎類型類型為基本的數值類型,支持位運算,因此可以使用一個值表示多個枚舉的組合,在定義枚舉時需要指定枚舉數為2的冪指數方便進行位運算,即枚舉數為1,2,4,8…,或1,1<<
Java中對Arrays陣列工具類的常用操作
Arrays類是JDK提供的專門用於運算元組的工具類,位於java.util包中。 用Arrays類的方法運算元組,無需自己編碼。 Arrays類的常用方法: 1、boolean equals(array1,array2):比較兩個陣列是否相等。 /** * 陣
24、python對資料框進行分組統計簡單操作
分組分析:是指根據分組欄位,將分析物件劃分成不同的部分,已進行對比分析各組之間的差異性的一種分析方法 常見的統計指標: 計數 求和 平均值 1 函式 01 分組統計函式: groupby(by=[分組列1,分組列2,...])[統計列1,統計列2,。。。] .agg({統計列名1:統計函
在Winform開發框架中對附件檔案進行集中歸檔處理
在我們Winform開發中,往往需要涉及到附件的統一管理,因此我傾向於把它們獨立出來作為一個附件管理模組,這樣各個模組都可以使用這個附件管理模組,更好的實現模組重用的目的。在涉及附件管理的場景中,一個數據記錄可能對應多個附件組場景,每個附件組則涉及附件多個檔案,往往這些附件可能放置的目錄會有所不同,
C++對MySQL資料庫進行匯出和匯入操作
相信大家應該在網上看到很多利用SQL語句對MySQL資料庫的表或者資料庫本身進行匯出和匯入操作。在window環境下利用dos命令列匯出資料庫(我的mysq直接裝在c盤根目錄下下,其bin目錄為:c:/mysql/bin): 1、執行MySQL資料庫匯出操作: c:\my
Audition頻率分析的Matlab實現程式碼:對同一個音源,matlab計算結果與Audition相同
Audition是音訊工程師廣泛使用的音訊分析處理軟體。Audition的頻率分析模組能夠得到音訊序列的頻譜圖,通過編寫matlab,實現類似Audition的結果。 大致思路是:對序列進行分幀,加窗,新增恢復係數,FFT,abs,平均。 保證matlab的結果與audition相同。
Linux 中對升級程式進行數字認證
最近專案中對升級的程式需要做認證,確保升級的資料包是本專案的升級程式包。 數字簽名:對某個資料塊的簽名,就是計算資料塊的Hash值,然後使用私鑰對hash值進行加密,結果就叫數字簽名,Hash值就是資料塊的數字指紋。 簽名驗證:資料接收者拿到原始資料塊與數字簽名後,接受者也會使用
使用dom4j對xml檔案進行讀取和輸出操作
1.xml檔案的讀取 讀取xml檔案的方式有兩種,一種是面向模型的DOM方式,一種是面向事件的SAX方式 DOM方式原理:一次性的將xml文件加入記憶體,在記憶體中形成一顆dom樹,然後通過語言對樹的節點進行操作。 顯然這種操作查詢元素快,但
Mysql中對資料統計進行排行榜排行
直接先上一個demo result表是一個儲存賽事結果的表 通過match_name和type_name來對不同的場次的比賽進行區分,result欄位為 SELECT t.id, @rownum :[email protected] + 1 AS ranking F