Java中的CopyOnWriteArrayList
类是List接口的线程安全实现。CopyOnWriteArrayList
是在Java 1.5中作为Collections框架的一部分中添加的。
Java ArrayList和ConcurrentModificationException
ArrayList
是List
接口的基本实现之一,它是Java Collections框架的一部分。可以使用迭代器遍历ArrayList
元素。
下面来看看ArrayList
的示例程序。
文件: ConcurrentListExample.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.OnWriteArrayList;
public class ConcurrentListExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
// get the iterator
Iterator<String> it = list.iterator();
//manipulate list while iterating
while(it.hasNext()){
System.out.println("list is:"+list);
String str = it.next();
System.out.println(str);
if(str.equals("2"))list.remove("5");
if(str.equals("3"))list.add("3 found");
//below code don't throw ConcurrentModificationException
//because it doesn't change modCount variable of list
if(str.equals("4")) list.set(1, "4");
}
}
}
当运行上面的程序时,一旦修改了ArrayList,就会抛出java.util.ConcurrentModificationException
异常。
这是因为ArrayList
迭代器的设计是快速失败的。也就是一旦创建了迭代器,如果修改了ArrayList
,它将抛出ConcurrentModificationException
异常。
如果查看控制台日志,应该注意到Iterator next()
方法抛出了异常。如果查看ArrayList
源代码,则每次调用抛出异常的迭代器上的next()
时都会调用以下方法。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这里modCount
是包含修改计数的ArrayList
变量,每次使用add
,remove
或trimToSize
方法时,它都会递增。expectedModCount
是与modCount
具有相同值的迭代器时初始化的迭代器变量。它解释了为什么我们使用set
方法替换元素时不会出现异常。
因此,如果更改列表大小,基本上迭代器会抛出ConcurrentModificationException
异常。
Java CopyOnWriteArrayList类
有时想要在列表中添加或删除元素,如果要查找一些特定元素,在这种情况下应该使用并发集合类 - CopyOnWriteArrayList
类。它是java.util.ArrayList
的线程安全变体,其中所有可变操作(添加,设置等)都是通过创建底层数组的新副本来实现的。
CopyOnWriteArrayList
类为引入了额外的重载,但是当修改次数与遍历操作次数相比最小时,它非常有效。
如果我们将实现更改为CopyOnWriteArrayList
,那么不会有任何异常,下面是生成的输出。
list is:[1, 2, 3, 4, 5]
1
list is:[1, 2, 3, 4, 5]
2
list is:[1, 2, 3, 4]
3
list is:[1, 2, 3, 4, 3 found]
4
list is:[1, 4, 3, 4, 3 found]
5
请注意,它允许修改列表,但它不会更改迭代器,获得的元素与原始列表中的元素相同。
使用for循环避免java.util.ConcurrentModificationException异常
如果处理单线程环境并希望代码处理列表中额外添加的对象,那么可以使用for
循环而不是Iterator
。
for(int i = 0; i<myList.size(); i++){
System.out.println(myList.get(i));
if(myList.get(i).equals("3")){
myList.remove(i);
i--;
myList.add("6");
}
}
请注意,减少计数器,因为要删除相同的对象,如果删除下一个或更远的对象,那么不需要减少计数器。
注意:如果使用subList
修改原始列表的结构,将会引发ConcurrentModificationException
异常。通过下面一个简单的例子来理解。
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationExceptionWithArrayListSubList {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Java");
names.add("PHP");
names.add("SQL");
names.add("Angular 2");
List<String> first2Names = names.subList(0, 2);
System.out.println(names + " , " + first2Names);
names.set(1, "JavaScript");
// check the output below. :)
System.out.println(names + " , " + first2Names);
// Let's modify the list size and get ConcurrentModificationException
names.add("NodeJS");
System.out.println(names + " , " + first2Names); // this line throws exception
}
}
执行上面示例代码,得到以下结果 -
[Java, PHP, SQL, Angular 2] , [Java, PHP]
[Java, JavaScript, SQL, Angular 2] , [Java, JavaScript]
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1282)
at java.base/java.util.ArrayList$SubList.listIterator(ArrayList.java:1151)
at java.base/java.util.AbstractList.listIterator(AbstractList.java:311)
at java.base/java.util.ArrayList$SubList.iterator(ArrayList.java:1147)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:465)
at java.base/java.lang.String.valueOf(String.java:2801)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.yiibai.ConcurrentModificationException.ConcurrentModificationExceptionWithArrayListSubList.main(ConcurrentModificationExceptionWithArrayListSubList.java:26)
根据ArrayList
文档,仅允许在subList
方法返回的列表上进行结构修改。返回列表中的所有方法首先检查后备列表的实际modCount
是否等于其预期值,如果不等于则抛出ConcurrentModificationException
异常。