CopyOnWriteArrayList实现了List接口,从名字可以看出它在写入数据的时候复制一份数组。
CopyOnWriteArrayList是数组结构,写数据可以大概描述为首先获取锁,接着把旧数组的数据复制到新数组,然后往新数组里插入数据,最后把list的数组替换为新数组。读数据不会加锁,直接读取数组的数据。
下面我们从代码层面去理解。
一、基本代码结构
以下代码可以看出CopyOnWriteArrayList是数组的数据结构
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** 重入锁用在修改数据的时候 */
final transient ReentrantLock lock = new ReentrantLock();
/** 保存数据的数组,由于是会用在并发环境,所以用了volatile修饰 */
private transient volatile Object[] array;
/** 设置数组,用在替换旧数组以及初始化 */
final void setArray(Object[] a) {
array = a;
}
/** 构造器会创建一个空的数组 */
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
}
二、插入数据
- add(E e)方法
从下面方法中看到,每添加一个对象,都需要获取锁,并且需要复制一次数组,如果是写入操作过多的话是很耗性能的。
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;
// 把list维护的旧数组替换为新的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
- add(int index, E element)方法
指定位置插入数据的话可能需要复制两次数组,更耗性能。
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();
}
}
三、获取数据
获取数据的方法一般是用get方法,从下面代码中看到它非常简单,直接就是从数组取出指定下标的对象,注意如果下标越界的话会抛出IndexOutOfBoundsException。
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
四、删除数据
1.remove方法
跟add(int index, E element)方法类似,可能需要复制两次数组。
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
2.clear方法
clear方法首先加锁,然后直接把数组设置为新的空数组,比较简单。
public void clear() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
setArray(new Object[0]);
} finally {
lock.unlock();
}
}
五、遍历
从下面代码中看到,遍历是取数组的一个快照,然后操作这个快照遍历数据。我们看到迭代器中不予许add、remove等修改操作。另外,如果是调用外部方法add、remove等修改数据的操作,由于修改都是新复制旧数组的数据到新数组,然后操作新数组,所以外部修改操作也不会影响迭代器的遍历,即遍历保证不会抛出ConcurrentModificationException。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
/** 迭代器不予许修改 */
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
}
六、总结
从以上对CopyOnWriteArrayList的分析中,我们可以得出以下几个结论:
- 数据结构:数组。
- 并发加锁:修改操作需要加重入锁,读取不需要加锁。
- 修改逻辑:复制数组数据,生产新的数组。
- 遍历:使用数组快照,保证不会抛出ConcurrentModificationException。
- 是否可插入空数据:可插入null对象。
- 使用场景:并发环境,多读少写。