1. 程式人生 > >程式碼中對同一個list進行操作增加刪除的操作,有多種情況,請根據需要選擇,否則入坑

程式碼中對同一個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.modCountthread1 iterator.expectedModCountthread2 iterator.expectedModCount
thread start,初始化iterator202020
thread2.remove()呼叫之後212021

兩個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

JavaString字符串的常用操作

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<<

JavaArrays陣列工具類的常用操作

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值就是資料塊的數字指紋。  簽名驗證:資料接收者拿到原始資料塊與數字簽名後,接受者也會使用

使用dom4jxml檔案進行讀取和輸出操作

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