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,在多线程环境中读多写少场景是比较有效的,它使用写时变更到新数组,然后修改引用指向,读还是在旧数组上读,实现读写分离,写和读有所延迟,数据不保证实时一致性,只能保证最终一致性,另外需要注意这种拷贝对象会耗费不少的内存。