之前我们分析了ArrayList的源码,但它不是线程安全的,多线程环境下一些问题,比如并发导致数据丢失,并发导致插入null,并发导致数组越界等。所以今天我们来分析线程安全的CopyOnWriteArrayList,CopyOnWrite也就是写时复制。写时复制的思想是当我们往一个容器添加或者删除元素的时候,不直接往当前容器添加,而是将当前容器复制出一个新的容器,然后在新的容器里进行操作,操作完成之后,再将原来容器的引用指向新的容器。这样做的好处就是读写分离,读不加锁,写加锁,读写不冲突,提高并发性,比较适合读多写少的场景。
先来看看继承关系
CopyOnWriteArrayList与ArrayList实现的接口是一样的,实现了List, RandomAccess, Cloneable, java.io.Serializable等接口。
2.1 属性
/**
* 对数组增删改时,用来加锁
*/
final transient ReentrantLock lock = new ReentrantLock();
/**
* 存储元素的地方,使用volatile修饰,在写操作替换新数组完成之后,读操作能找到替换的新数组
*/
private transient volatile Object[] array;
2.2 构造函数
public CopyOnWriteArrayList() {
// 构建一个长度为0的数组,
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
// 如果c是CopyOnWriteArrayList类型
if (c.getClass() == CopyOnWriteArrayList.class)
// 那么直接使用c的数
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
// c不是CopyOnWriteArrayList类型,调用toArray()方法将集合元素转化为数组
elements = c.toArray();
// elements不是Object[]类型,转换为Object[]类型
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
//把任意类型数组中的元素拷贝到Object[]类型的数组中。
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
2.3 add(E e)方法
添加一个元素到末尾
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 获取原来的数组
Object[] elements = getArray();
int len = elements.length;
// 构建一个新数组,新数组大小是旧数组大小加1
// 将旧数组元素拷贝到新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 将元素放在最后一位
newElements[len] = e;
//将array引用指向新的数组,原来的数组会被回收
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;
// numMoved为0,说明插入的位置是最后一位
if (numMoved == 0)
// 那么拷贝一个n+1的数组, 其前n个元素与旧数组一致
newElements = Arrays.copyOf(elements, len + 1);
else {
// numMoved不为0,说明插入的位置不是最后一位,那么新建一个n+1的数组
newElements = new Object[len + 1];
// 拷贝旧数组前index个元素到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
// 将index及其之后的元素往后挪一位拷贝到新数组中,这样index的位置上是空的
System.arraycopy(elements, index, newElements, index + 1, numMoved);
}
// 将元素放置在index处
newElements[index] = element;
setArray(newElements);
} finally {
// 释放锁
lock.unlock();
}
}
2.4 remove(int index)方法
删除指定索引位置的元素。
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);
// 跟add方法一样,计算需要移动的元素个数
int numMoved = len - index - 1;
// numMoved为0,需要移除的是最后一位
if (numMoved == 0)
// 直接拷贝前面n-1元素形成的新数组, 最后一位就自动删除了
setArray(Arrays.copyOf(elements, len - 1));
else {
// 如果移除的不是最后一位,新建一个n-1的新数组
Object[] newElements = new Object[len - 1];
// 将前index的元素拷贝到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
// 将index+1以及后的元素往前挪一位,这样原来index位置上的元素就被覆盖掉了, 相当于删除
System.arraycopy(elements, index + 1, newElements, index, numMoved);
setArray(newElements);
}
return oldValue;
} finally {
// 释放锁
lock.unlock();
}
}
2.5 set(int index, E element)方法
修改index索引上的元素
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
Object[] elements = getArray();
// 获取index位置上原来的元素值
E oldValue = get(elements, index);
// 原来的元素的值不等于新值,则复制一份到新数组中再进行修改
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// 确保set的写语义
setArray(elements);
}
return oldValue;
} finally {
//释放锁
lock.unlock();
}
}
2.6 get(int index)方法
获取index索引上的元素
public E get(int index) {
// 获取元素不加锁,直接返回index位置的元素
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
因为get()方法不需要加锁,所以会出现这样一种情况,set()修改了数组中元素的值,但是还没有把将array引用指向新的数组,这时候get()方法会去原来的数组中获取,得到还没修改的值,这样就导致了数据的不一致,如果在set()方法将array引用指向新的数组之后,再去调用get()去获取值,就能得到修改后的值 ,所以说CopyOnWriteArrayList不是数据强一致性的,而是最终一致性的。
(1) CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作使得占用内存翻倍,所以适用于读多写少的场合;
(2)CopyOnWriteArrayList的写操作,先获取到ReentrantLock锁,保证线程安全,然后拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下;
(3)CopyOnWriteArrayList的读操作不加锁,可能获取不到最新修改的元素,只保证最终一致性,不保证实时一致性。