ArrayList是List接口的基本实现之一,它是Java Collections Framework的一部分。我们可以使用迭代器遍历ArrayList元素。
我们看看一下ArrayList的示例程序:
package com.roin.concurrent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ConcurrentListExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
// 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("B"))
list.remove("E");
if(str.equals("C"))
list.add("C found");
//below code don't throw ConcurrentModificationException
//because it doesn't change modCount variable of list
if(str.equals("D"))
list.set(1, "D");
}
}
}
运行结果:
D:\Java\jdk1.8.0_102\bin\java -Didea.launcher.port=7539 "-Didea.launcher.bin.path=D:\Program Files\JetBrains\IntelliJ IDEA 15.0\bin" -Dfile.encoding=UTF-8 -classpath "D:\Java\jdk1.8.0_102\jre\lib\charsets.jar;D:\Java\jdk1.8.0_102\jre\lib\deploy.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\access-bridge.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_102\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_102\jre\lib\javaws.jar;D:\Java\jdk1.8.0_102\jre\lib\jce.jar;D:\Java\jdk1.8.0_102\jre\lib\jfr.jar;D:\Java\jdk1.8.0_102\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_102\jre\lib\jsse.jar;D:\Java\jdk1.8.0_102\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_102\jre\lib\plugin.jar;D:\Java\jdk1.8.0_102\jre\lib\resources.jar;D:\Java\jdk1.8.0_102\jre\lib\rt.jar;F:\Javapackage\dubbo\concurrent\out\production\concurrent;D:\Program Files\JetBrains\IntelliJ IDEA 15.0\lib\idea_rt.jar" com.intellij.rt.execution.application.AppMain com.roin.concurrent.ConcurrentListExample
list is:[A, B, C, D, E]
A
list is:[A, B, C, D, E]
B
list is:[A, B, C, D]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.roin.concurrent.ConcurrentListExample.main(ConcurrentListExample.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Process finished with exit code
我们运行上面的程序会报ConcurrentModificationException异常,发生这种情况是因为ArrayList迭代器在设计上是快速失效的。这意味着一旦创建了迭代器,如果修改了ArrayList,则会抛出ConcurrentModificationException。
我们注意到异常是由Iterator next()方法抛出的,现在我们看看源码中 next()里做了什么?
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
再看checkForComodification方法里面做了什么?
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
expectedModCount是我们创建迭代器时初始化的迭代器变量,而里modCount是保存修改次数。我们每次使用ArrayList的add,remove或trimToSize都会修改这个值,当检查到不一致时就会抛出上述异常,因此ArrayList是非安全的。
可以一看一下是在创建迭代器时初始化值的expectedModCount是在创建迭代器时初始化值的
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
有时我们想要添加或删除列表中的元素,那么我们应该使用并发集合类 - CopyOnWriteArrayList。这是java.util.ArrayList的线程安全变体,其中所有可变操作(add,set等)都是通过创建底层数组的新副本来实现的。
CopyOnWriteArrayList为处理引入了额外的性能消耗,但与遍历操作的次数相比,修改的次数很少时,它非常有效。
如果我们把上面程序ArrayList改成CopyOnWriteArrayList,那么我们不会得到任何异常。
现在看运行结果:
我们可以看,list中的元素被改变了,但是迭代的元素还是原来list中的值。
说明:CopyOnWriteArrayList为什么能做到迭代的同时还能进行数据修改,首先根据其名字CopyOnWrite,也就是在写的时候会拷贝一个新的Object数组,然后对新数组进行修改,修改完成后,再将旧数组的引用指向这个新的数组,那么读呢?读还是在旧的数组上读,可以看出往数组里面写和读之间是有时间差的,不是实时的。
我们看看源码中几个关键点:
1.数组是使用volatile修饰的,也就意味着array数组一旦修改,其他线程迭代该数组马上就能拿到值。
private transient volatile Object[] array;
2.读并未加锁,多个线程读操作不会有线程安全问题,多个线程读取会,读取到旧的数据。
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
3.写操作使用了锁,我们看看add方法,其它类似
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
这里加锁的目的是多线程防止拷贝多个数组,我们看到拷贝工作流程,
首先获取原来数组长度,然后拷贝产生一个大小为原来数组+1个元素的新数组,
然后在新数组的最后一个索引位置设置我们要添加的值,
最后把array的引用指向新的数组。
看下setArray方法
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
总结一下吧:
ArrayList在迭代的过程中,如果发生增加,删除等导致modCount发生变化的操作时会抛出异常。
CopyOnWriteArrayList,在多线程环境中读多写少场景是比较有效的,它使用写时变更到新数组,然后修改引用指向,读还是在旧数组上读,实现读写分离,写和读有所延迟,数据不保证实时一致性,只能保证最终一致性,另外需要注意这种拷贝对象会耗费不少的内存。
优点:
1.解决的开发工作中的多线程的并发问题。
缺点:
1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。
2.数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器