天天看点

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》