天天看點

ArrayList引起的ConcurrentModificationException 異常原因及解決方法一、單線程二、多線程三、參考資料

一、單線程

1. 異常情況舉例

隻要抛出出現異常,可以肯定的是代碼一定有錯誤的地方。先來看看都有哪些情況會出現ConcurrentModificationException異常,下面以ArrayList remove 操作進行舉例:

使用的資料集合:

?

1 2 3 4 5 6 7

List<string> myList =

new

ArrayList<string>();

myList.add(

"1"

);

myList.add(

"2"

);

myList.add(

"3"

);

myList.add(

"4"

);

myList.add(

"5"

);</string></string>

以下三種情況都會出現異常: ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

Iterator<string> it = myList.iterator();

while

(it.hasNext()) {

String value = it.next();

if

(value.equals(

"3"

)) {

myList.remove(value); 

// error

}

}

for

(Iterator<string> it = myList.iterator(); it.hasNext();) {

String value = it.next();

if

(value.equals(

"3"

)) {

myList.remove(value); 

// error

}

}

for

(String value : myList) {

System. out.println(

"List Value:"

+ value);

if

(value.equals(

"3"

)) {

myList.remove(value); 

// error

}

}   </string></string>

異常資訊如下:

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.AbstractList$Itr.checkForComodification(Unknown Source)

at java.util.AbstractList$Itr.next(Unknown Source)

2. 根本原因

以上都有3種出現異常的情況有一個共同的特點,都是使用Iterator進行周遊,且都是通過ArrayList.remove(Object) 進行删除操作。

想要找出根本原因,直接檢視ArrayList 源碼看為什麼出現異常:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

public

class

ArrayList<e>

extends

AbstractList<e>

implements

Cloneable, Serializable, RandomAccess {

@Override

public

boolean

remove(Object object) {

Object[] a = array;

int

s = size;

if

(object !=

null

) {

for

(

int

i =

; i < s; i++) {

if

(object.equals(a[i])) {

System.arraycopy(a, i +

1

, a, i, --s - i);

a[s] =

null

// Prevent memory leak

size = s;

modCount++; 

// 隻要删除成功都是累加

return

true

;

}

}

}

else

{

for

(

int

i =

; i < s; i++) {

if

(a[i] ==

null

) {

System.arraycopy(a, i +

1

, a, i, --s - i);

a[s] =

null

// Prevent memory leak

size = s;

modCount++; 

// 隻要删除成功都是累加

return

true

;

}

}

}

return

false

;

}  

@Override

public

Iterator<e> iterator() {

return

new

ArrayListIterator();

}  

private

class

ArrayListIterator

implements

Iterator<e> {

......

// 全局修改總數儲存到目前類中

private

int

expectedModCount = modCount;

@SuppressWarnings

(

"unchecked"

)

public

E next() {

ArrayList<e> ourList = ArrayList.

this

;

int

rem = remaining;

// 如果建立時的值不相同,抛出異常

if

(ourList.modCount != expectedModCount) {

throw

new

ConcurrentModificationException();

}

if

(rem ==

) {

throw

new

NoSuchElementException();

}

remaining = rem -

1

;

return

(E) ourList.array[removalIndex = ourList.size - rem];

}  

......

}

}   </e></e></e></e></e>

Iterator是工作在一個獨立的線程中,并且擁有一個mutex鎖,就是說Iterator在工作的時候,是不允許被疊代的對象被改變的。Iterator被建立的時候,建立了一個記憶體索引表(單連結清單),這個索引表指向原來的對象,當原來的對象數量改變的時候,這個索引表的内容沒有同步改變,是以當索引指針往下移動的時候,便找不到要疊代的對象,于是産生錯誤。List、Set等是動态的,可變對象數量的資料結構,但是Iterator則是單向不可變,隻能順序讀取,不能逆序操作的資料結構,當Iterator指向的原始資料發生變化時,Iterator自己就迷失了方向。

List、Set、Map 都可以通過Iterator進行周遊,這裡僅僅是通過List舉例,在使用其他集合周遊時進行增删操作都需要留意是否會觸發ConcurrentModificationException異常。

3. 解決方案

上面列舉了會出現問題的幾種情況,也分析了問題出現的根本原因,現在來總結一下怎樣才是正确的,如果避免周遊時進行增删操作不會出現ConcurrentModificationException異常。

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50

// 1 使用Iterator提供的remove方法,用于删除目前元素

for

(Iterator<string> it = myList.iterator(); it.hasNext();) {

String value = it.next();

if

(value.equals(

"3"

)) {

it.remove(); 

// ok

}

}

System. out.println(

"List Value:"

+ myList.toString());

// 2 建一個集合,記錄需要删除的元素,之後統一删除            

List<string> templist =

new

ArrayList<string>();

for

(String value : myList) {

if

(value.equals(

"3"

)) {

templist.remove(value);

}

}

// 可以檢視removeAll源碼,其中使用Iterator進行周遊

myList.removeAll(templist);

System. out.println(

"List Value:"

+ myList.toString());       

// 3. 使用線程安全CopyOnWriteArrayList進行删除操作

List<string> myList =

new

CopyOnWriteArrayList<string>();

myList.add(

"1"

);

myList.add(

"2"

);

myList.add(

"3"

);

myList.add(

"4"

);

myList.add(

"5"

);

Iterator<string> it = myList.iterator();

while

(it.hasNext()) {

String value = it.next();

if

(value.equals(

"3"

)) {

myList.remove(

"4"

);

myList.add(

"6"

);

myList.add(

"7"

);

}

}

System. out.println(

"List Value:"

+ myList.toString());

// 4. 不使用Iterator進行周遊,需要注意的是自己保證索引正常

for

(

int

i =

; i < myList.size(); i++) {

String value = myList.get(i);

System. out.println(

"List Value:"

+ value);

if

(value.equals(

"3"

)) {

myList.remove(value); 

// ok

i--;

// 因為位置發生改變,是以必須修改i的位置

}

}

System. out.println(

"List Value:"

+ myList.toString());</string></string></string></string></string></string>

輸出結果都是:List Value:[1, 2, 4, 5] , 不會出現異常。

以上4種解決辦法在單線程中測試完全沒有問題,但是如果在多線程中呢?

二、多線程

1. 同步異常情況舉例

上面針對ConcurrentModificationException異常在單線程情況下提出了4種解決方案,本來是可以很哈皮的洗洗睡了,但是如果涉及到多線程環境可能就不那麼樂觀了。

下面的例子中開啟兩個子線程,一個進行周遊,另外一個有條件删除元素:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

final

List<string> myList = createTestData();

new

Thread(

new

Runnable() {

@Override

public

void

run() {

for

(String string : myList) {

System.out.println(

"周遊集合 value = "

+ string);

try

{

Thread.sleep(

100

);

}

catch

(InterruptedException e) {

e.printStackTrace();

}

}

}

}).start();

new

Thread(

new

Runnable() {

@Override

public

void

run() {

for

(Iterator<string> it = myList.iterator(); it.hasNext();) {

String value = it.next();

System.out.println(

"删除元素 value = "

+ value);

if

(value.equals(

"3"

)) {

it.remove();

}

try

{

Thread.sleep(

100

);

}

catch

(InterruptedException e) {

e.printStackTrace();

}

}

}

}).start();</string></string>

輸出結果:

周遊集合 value = 1

删除元素 value = 1

周遊集合 value = 2

删除元素 value = 2

周遊集合 value = 3

删除元素 value = 3

Exception in thread "Thread-0" 删除元素 value = 4

java.util.ConcurrentModificationException

at java.util.AbstractList$Itr.checkForComodification(Unknown Source)

at java.util.AbstractList$Itr.next(Unknown Source)

at list.ConcurrentModificationExceptionStudy$1.run(ConcurrentModificationExceptionStudy.java:42)

at java.lang.Thread.run(Unknown Source)

删除元素 value = 5

結論:

上面的例子在多線程情況下,僅使用單線程周遊中進行删除的第1種解決方案使用it.remove(),但是測試得知4種的解決辦法中的1、2、3依然會出現問題。

接着來再看一下 JavaDoc對java.util.ConcurrentModificationException異常的描述:

當方法檢測到對象的并發修改,但不允許這種修改時,抛出此異常。

說明以上辦法在同一個線程執行的時候是沒問題的,但是在異步情況下依然可能出現異常。

2. 嘗試方案

(1) 在所有周遊增删地方都加上synchronized或者使用Collections.synchronizedList,雖然能解決問題但是并不推薦,因為增删造成的同步鎖可能會阻塞周遊操作。

(2) 推薦使用ConcurrentHashMap或者CopyOnWriteArrayList。

3. CopyOnWriteArrayList注意事項

(1) CopyOnWriteArrayList不能使用Iterator.remove()進行删除。

(2) CopyOnWriteArrayList使用Iterator且使用List.remove(Object);會出現如下異常:

java.lang.UnsupportedOperationException: Unsupported operation remove

at java.util.concurrent.CopyOnWriteArrayList$ListIteratorImpl.remove(CopyOnWriteArrayList.java:804)

4. 解決方案

單線程情況下列出4種解決方案,但是發現在多線程情況下僅有第4種方案才能在多線程情況下不出現問題。

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

List<string> myList =

new

CopyOnWriteArrayList<string>();

myList.add(

"1"

);

myList.add(

"2"

);

myList.add(

"3"

);

myList.add(

"4"

);

myList.add(

"5"

);

new

Thread(

new

Runnable() {

@Override

public

void

run() {

for

(String string : myList) {

System.out.println(

"周遊集合 value = "

+ string);

try

{

Thread.sleep(

100

);

}

catch

(InterruptedException e) {

e.printStackTrace();

}

}

}

}).start();

new

Thread(

new

Runnable() {

@Override

public

void

run() {

for

(

int

i =

; i < myList.size(); i++) {

String value = myList.get(i);

System.out.println(

"删除元素 value = "

+ value);

if

(value.equals(

"3"

)) {

myList.remove(value);

i--;

// 注意                          

}

try

{

Thread.sleep(

100

);

}

catch

(InterruptedException e) {

e.printStackTrace();

}

}

}

}).start();</string></string>

輸出結果:

删除元素 value = 1

周遊集合 value = 1

删除元素 value = 2

周遊集合 value = 2

删除元素 value = 3

周遊集合 value = 3

删除元素 value = 4

周遊集合 value = 4

删除元素 value = 5

周遊集合 value = 5

OK,搞定

三、參考資料

《How to Avoid ConcurrentModificationException when using an Iterator》