写时复制的原理
所有的读,均直接从数组中获取数据。所有的写,通过先复制数组得到副本,修改副本,然后重新指向新数组的步骤。适用于“读多写少”的场景。
写时复制的缺点
CopyOnWriteArrayList每次执行写操作都会将原容器进行拷贝一份,数据量大的时候,内存会存在较大的压力,可能会引起频繁Full GC。
写和读分别作用在不同新老容器上,在写操作执行过程中,读不会阻塞,但读取到的却是老容器的数据。(不是最新的数据,相当于快照)
写时复制的实现
新增元素:
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();
}
}
删除元素:
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)
// 如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
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();
}
}
读:
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
由此可见,写时复制技术付出内存double的代价,换取读不加锁的高性能。
|