暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

多线程下探究并发修改异常

Serenest Person 2020-03-28
390
上次我们分析了在单线程情况下,用迭代器方式遍历,删除元素时的并发修改异常问题。在单线程的情况下,我们使用迭代器对象的remove代替list的remove即可。可是在多线程情况下,依然可以吗?我们试一下。
ArrayList<Integer> contents = new ArrayList<>();
for (int i = 0; i < 100; i++) {
contents.add(i);
}
new Thread(()->{
Iterator<Integer> iterator = contents.iterator();
    System.out.println(Thread.currentThread().getName()+"::");
while (iterator.hasNext()) {
Integer next = iterator.next();
//contents.remove(next);
System.out.print(next);
}
System.out.println();
},"线程1").start();
new Thread(()->{
Iterator<Integer> iterator = contents.iterator();
System.out.println(Thread.currentThread().getName()+"::");
while (iterator.hasNext()) {
Integer next = iterator.next();
if (next%2==0) {
//contents.remove(next);
System.out.print(next);
iterator.remove();
}
}
},"线程2").start();
代码很简单,准备一个集合,添加0-99数字元素。准备两个线程,线程1遍历集合,线程2删除集合中的偶数。结果如下:
线程1::0,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,
线程2::0,2,4,6,8,46,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,
Exception in thread "线程1" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.company.Main.lambda$main$0(Main.java:312)
at java.lang.Thread.run(Thread.java:748)
我们发现,在线程1遍历打印集合的过程中,出现了并发修改异常。但是我们确实是使用的 iterator.remove();来删除元素的。为何多线程情况下还是出现这个异常?其实我们多运行几次程序,并不是每次都报错,这取决于两个线程被调度的情况。我们来分析下原因。

上篇我们分析了ConcurrentModificationException异常是因为expectedModCountmodCount不相等导致。我们知道modCount是AbstarctList的成员变量,expectedModCount是迭代器对象的私有变量。也就是说线程1,和线程2都可以改变共有的modCount,而线程1,和线程2各自的迭代器对象中的expectedModCount是不同的。在我们上面的测试代码中,线程2修改了集合,线程1没有修改,因此,当线程2执行完,切换到线程1的时候,线程1的迭代器对象的expectedModCount肯定和modCount是不想等的,因此报出异常。

有人会说换成Vector就行了,ArrayList是线程不安全的,Vector是线程安全的。其实Vector也不行。因为用Iterator去遍历是不需要获取所对象的,因此结果也是一样的。

那么我们怎么解决多线程下的这个问题呢?当然是线程同步。下面我们改造下代码,当然线程同步的方式有很多,synchronized,Lock等等,这里我们使用synchronized。代码如下:

ArrayList<Integer> contents = new ArrayList<>();
for (int i = 0; i < 100; i++) {
contents.add(i);
}
//创建同步对象
Object o = new Object();
new Thread(() -> {
synchronized (o) {
Iterator<Integer> iterator = contents.iterator();
System.out.println(Thread.currentThread().getName());
while (iterator.hasNext()) {
Integer next = iterator.next();
//contents.remove(next);
System.out.print(next+",");
}
}
},"线程1").start();
new Thread(() -> {
synchronized (o) {
Iterator<Integer> iterator = contents.iterator();
System.out.println(Thread.currentThread().getName());
while (iterator.hasNext()) {
Integer next = iterator.next();
if (next%2==0) {
//contents.remove(next);
iterator.remove();
}
}
System.out.println(contents);
}
},"线程2").start();
运行结果如下:
线程2
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]
线程1
1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,
Process finished with exit code 0
我们发现问题完全解决了。加上线程同步以后,需要等一个线程完全执行完,另一个线程才可以执行。这样两个线程的iterator中的expectedModCount和父类的modCount是相等的,其实也就相当于是单线程的情况。
其实还有一种方法是使用并发容器,比如CopyOnWriteArrayList,并发容器在做add和remove的时候,都加了锁,这个我们后面找时间详细分析,今天就到这儿。


文章转载自Serenest Person,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论