Copy-On-Write简称COW,是一种用于程序设计中的优化策略。很多时候我们修改一个重要的文件时都会先把文件复制一份作为备份,然后才对原文件进行修改,这样既可以防止修改错误无法恢复到原来正确的状态,也防止计算机突然断电造成数据不一致。当我们确定修改没有问题时会把原来的备份删掉(当然也可以保留下来),然后把修改后的文件再复制一份作为备份,再在当前的基础上进行修改,修改完成后再覆盖原来的备份!
CopyOnWriteArrayList概述
CopyOnWrite容器即写时复制的容器,在写如数据时,先复制一套原来的的数据,在新的数据里面进行写入,写入完成之后,再把这个数据覆盖到原来的数据上,这样保证了读取数不受影响,但是新写入的数据因为是写入后再复制,所以会稍微延迟。
CopyOnWriteArrayList源码
CopyOnWriteArrayList的类继承结构图,发现其是List集合的派系。
其源码:
/** The array, accessed only via getArray/setArray. array用来存放数据*/
private transient volatile Object[] array;
使用加锁机制保证并发安全:
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
跟踪其是如何获取集合中的数据的:
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
可以看出其读取数据时,是直接通过getArray()方法活的array数据里面直接获取的,并没有加锁,所以速度很快。
而跟踪其往list集合中添加数据时:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 复制一份array对象数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 往新复制的对象数据中插入要添加的元素
newElements[len] = e;
// 用新复制的对象,覆盖list集合中的数组对象
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
往集合中固定位置添加数据:
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
// 复制一份array对象数组
Object[] newElements = Arrays.copyOf(elements, len);
// 往新复制的对象数据中插入要添加的元素
newElements[index] = element;
// 用新复制的对象,覆盖list集合中的数组对象
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
// 返回老对象
return oldValue;
} finally {
lock.unlock();
}
}
源码实现上并不复杂,了解了其原理,看下其使用场景
适用场景
通过源码add()和get()方法可以看出,CopyOnWriteArrayList是比较适合读多写少的,且对数据实时性要求不是很高的场景下。一般这样的场景有白名单黑名单,导航菜单等。
比如一个黑名单使用案例:
public class BlackIp {
/* IP黑名单集 */
private static CopyOnWriteArrayList<String> blackList = new CopyOnWriteArrayList<String>();
static {
blackList.add("192.168.0.1");
blackList.add("192.168.0.1");
}
public boolean add(String ip){
// validIp(ip);
return blackList.add(ip);
}
public boolean contains(String ip){
return blackList.contains(ip);
}
public boolean remove(String ip){
return blackList.remove(ip);
}
}
也因为其往集合插入数据时,是拷贝了一份数据,如果占用内存大,当集合中数据逐渐变多了,会耗内存,同时还需要注意以下两点:
- 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
- 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。