这个寒假看的《Java并发编程实战》终于派上了用场,今天搬砖过程中遇到了ConcurrentModificationException,结果还是不会。之后晚上回去恶补一波,写此文章。

JCP -> 《Java并发编程实战》

Java并发-目录

Single Thread

JCP 第五章:在单线程代码中也可能抛出ConcurrentModificationException。当对象直接从容器中删除而不是通过Iterator.remove来删除时,就会抛出这个异常。

虽然ConcurrentModificationException这个异常听上去是多线程环境下出现的。但是如上面的引用,单线程里如果使用不当也会出现。

1
2
3
4
5
6
7
8
private final ArrayList<Integer> list = new ArrayList<>();
private void iterateAndRemove() {
for (Integer i : list) {
if (i % 4 == 0) {
list.remove(i);
}
}
}

在使用for-each遍历列表时,实际上就是用Iterator遍历,但在删除的时候使用了list.remove(),这就造成了ArrayList中的modCountIterator中的modCount不相同,从而抛出异常。

解决办法也很简单,只要使用Iterator.remove就不会有异常。所以即使在单线程中也不能混用Iterator和容器中的方法。

Multi Thread

多线程的情况下,情况变得更加复杂。我们来看下面的实例代码:

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
public class ConcurrentModificationExceptionTest {

private final ExecutorService executor = Executors.newFixedThreadPool(10);
private final ArrayList<Integer> list = new ArrayList<>();

// works
private final Runnable iteratorRunnable = () -> {
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
int i = it.next();
if (i % 4 == 0) {
it.remove();
System.out.println("Removed " + i);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

public ConcurrentModificationExceptionTest() {
for (int i = 0; i < 100; i++) {
list.add(i);
}
}

public void testArrayList() {
for (int i = 0; i < 20; i++) {
executor.execute(iteratorRunnable);
executor.execute(() -> {
for (Integer integer : list) {
System.out.println(integer);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}

public void stop() throws InterruptedException {
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
}

public static void main(String[] args) throws InterruptedException {
ConcurrentModificationExceptionTest test = new ConcurrentModificationExceptionTest();
test.testArrayList();
test.stop();
}
}

上面的代码也会造成ConcurrentModificationException,因为多个线程同时遍历列表,也可能造成ArrayList中的modCountIterator中的modCount不相同。

Solution

  • 迭代时对容器加锁
1
2
3
4
5
synchronized (list) {
for (Integer integer : list) {
//doSomething
}
}
  • 在副本上进行迭代
1
2
3
4
ArrayList<Integer> cloneList = (ArrayList<Integer>) list.clone();
for (Integer integer : cloneList) {
//doSomething
}
  • 使用CopyOnWriteArrayList

参考