9.2 ArrayList&CopyOnWriteArrayList详解
1.0 ArrayList
ArrayList是Java集合框架中的一个重要的类,它继承于AbstractList,实现了List接口,是一个长度可变的集合,提供了增删改查的功能,允许null的存在。ArrayList类还是实现了RandomAccess接口,可以对元素进行快速访问。实现了Serializable接口,说明ArrayList可以被序列化 。
ArrayList<String> list = new ArrayList();
list.add("a");
//线程向数组中添加数据
new Thread(()->{
for(int i=0;i<1000;i++){
list .add("i"+i) ;
}
}).start();
//线程读数据
new Thread(()->{
for ( String s: list) {
/* if("a".equals(s)){
list.remove(s) ;
}*/
System.out.println(s);
}
}).start();
上面的代码执行会报ConcurrentModificationException,为什么呢?因为Failfast机制。
1.1 Fail-fast 机制及ArrayList源码分析
快速失败系统,通常设计用于停止有缺陷的过程,这是一种理念,在进行系统设计时优先考虑异常情况,一旦发生异常,直接停止运行并上报。
ArrayList的 foreach 循环实际就是调用 ArrayList的iterator()方法循环,部分代码如下
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
final int offset = this.offset;
return new ListIterator<E>() {
int cursor = index;
int lastRet = -1;
//expectedModCount的初始化值。
int expectedModCount = ArrayList.this.modCount;
public boolean hasNext() {
return cursor != SubList.this.size;
}
@SuppressWarnings("unchecked")
public E next() {
//数组是否修改
checkForComodification();
int i = cursor;
if (i >= SubList.this.size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[offset + (lastRet = i)];
}
从源码可知,ArrayList的循环中多个方法的开始就是检测是否有修改,checkForComodification源码如下
final void checkForComodification() {
if (expectedModCount != ArrayList.this.modCount)
throw new ConcurrentModificationException();
}
从源码中可以发现,这个异常时变量expectedModCount和modCount不相等造成的,从代码中可以看出modCount是ArrayList的成员变量,表示集合被修改的次数,当ArrayList创建时就存在,初始值为0;expectedModCount是 iterator()迭代器方法的一个局部变量初始值和modCount一致,只有迭代器修改了集合,expectedModCount才会修改。看下ArrayList的remove方法源码如下:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
这个方法会修改modCount,所以会导致两个值不相等报出异常。add方法中modCount也会增加,源码的注释有,不相信自己可以去看源码。
从上面的代码逻辑推理如果想不报错,那么操作ArrayList尽量用迭代器来操作为什么呢?因为expectedModCount是迭代器中的局部变量,只有迭代器才能操作它。除了这种解决方法还有其他方法吗?有可以用copyOnWriteArrayList代替ArrayList,因为copyOnWriteArrayList是线程安全的。
2.0 CopyOnWriteArrayList
CopyOnWriteArrayList 是 ArrayList 的线程安全版本,读取无锁,写时有锁。适用于 写少读多的场景。
2.1 CopyOnWriteArrayList线程安全原理分析
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();
}
}
从上面的代码可知,安全的方式就是:在写时先加锁,在把数组复制一份,在新的数组上操作 ,最后用新数组覆盖原数组。
2.2 存在的问题及解决方法
CopyOnWriteArrayList用空间换时间的思想提高性能,因为写的操作都会复制出一个副本,若数组比较大,就会导致堆空间的内存急剧增加,频繁引发full GC,从而影响系统性能。
解决方法:用ReentrantLock 自定义一个线程安全的 ArrayList,分别定义个读锁和写锁,读写和写写用互斥锁 ,读读不互斥,这样即线程安全又能避免数据拷贝。