CopyOnWriteArrayList分析
ArrayList并没有考虑并发的情况,在多线程高并发访问ArrayList的情况下,它并不能保证线程安全。CopyOnWriteArrayList是ArrayList变种,而且它是线程安全的。CopyOnWriteArrayList的add(), remove(), set()等方法的实现都会创建基础数组的拷贝,并在新创建的数组上实现add, remove, set等修改操作,最后将新数组设置成基础数组。因此,旧的数组并没有被修改,其他线程对旧数组的访问依然是正常的,它不会受到新修改的影响。当然这是需要付出代价的,那就是每当修改容器时都会旧数组的同容全部拷贝一遍,这需要更多的内存,更多的读写数据时间开销,特别是当容器规模较大的时候。所以,CopyOnWriteArrayList适用于读操作远远多于写操作的场景,在这种情况下它能够提供更好的并发性能。另外,当你不能或者不想对遍历同步但又希望防止多线程并发的干扰时,CopyOnWriteArrayList就很有用了。
下面分析一下CopyOnWriteArrayList的主要方法:
get()方法
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
get()方法不需要加锁,它的实现也非常简单,只是调用了一个方法get(Object[] a, int index),获取基础数组index位置上的元素。
add()方法
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
这是带两个参数的add()方法,作用是在index的位置上插入一个element。由于这是修改操作,为了保证写操作是线程安全的,必须先获得锁。这里使用的是ReentrantLock的lock()方法获取锁。接着检查index的范围,如果超出了正常的范围,就抛出IndexOutOfBoundsException。如果index是指向最后一个元素,则将基础数组的所有元素复制到长度为len+1新数组;如果index指向中间的元素,则先把index前面的所有元素复制到新数组,空出新数组上index所指的位置用于容纳新元素,然后把index之后的所有元素复制到新数组。最后,在新数组的index位置添加element,将新数组设置为基础数组,释放锁。
CopyOnWriteArrayList的其他写操作set(), remove()等与add()方法的实现方式基本上是一致的,都需要获得ReentrantLock,复制底层数组到新数组,在新数组上进行修改,最后将新数组设为基础数组,释放锁。
CopyOnWriteArrayList内部提供了COWIterator。虽然它实现了ListIterator接口,但是它其他并不支持Iterator上的remove(), add()和set()操作,这些方法只是简单地抛出UnsupportedOperationException。COWIterator初始化的时候就获取了CopyOnWriteArrayList底层的基础数组,Iterator在遍历的过程中不支持对这个基础数组的修改,而其他线程在执行写操作的时候会将基础数组复制一遍,在新的基础数组上进行写操作。因此,无论其他线程执行哪种操作,COWIterator一完成初始化,它的基础数组拷贝就不会发生变化。