维基百科定义:
写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。
此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
大白话:
通俗的讲就是,cow基于“写时复制”的思想,当并发请求读取相同的数据资源时,读取的是同一份数据,当某个请求尝试去修改资源时,就会通过复制出一个副本进而去修改副本的数据,其他请求读取的数据还是最初不变的,最后将指向源数据的引用指向此线程修改后的副本数据。
Java中的Cow容器有两个,在Jdk1.5版本中开始出现的,分别是CopyOnWriteArrayList及CopyOnWriteArraySet。
CopyOnWriteArrayList与CopyOnWriteArraySet基本一致,主要区别是在add方法,CopyOnWriteArraySet,有set的特性,即存储元素的是不重复的,因此CopyOnWriteArraySet的add方法中使用的是addIfAbsent(E e),即只有当元素不存在的时候,才会将元素添加到集合的尾部。
从源码中,可以看出CopyOnWriteArrayList内部持有一个ReentrantLock锁,最重要属性array是一个Object类型的数组并且有volatile关键字修饰,而且这个array只能通过getArray()和setArray()来进行访问。
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/**
* 获取源数组
*/
final Object[] getArray() {
return array;
}
/**
*将源数组引用指向新数组
*/
final void setArray(Object[] a) {
array = a;
}
add()方法:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 1、加锁
lock.lock();
try {
// 2、获取源数组引用
Object[] elements = getArray();
int len = elements.length;
// 3、拷贝出一个新数组,新数组长度=源数组长度 + 1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 4、将元素添加到新数组的尾部
newElements[len] = e;
// 5、将源数组引用指向新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
add()方法主要工作流程如下:
remove()方法
public boolean remove(Object o) {
// 1、获取数组快照
Object[] snapshot = getArray();
// 2、获取要移除元素的索引下标
int index = indexOf(o, snapshot, 0, snapshot.length);
// 3、若未找到,则直接返回false,否则调用remove方法进行移除
return (index < 0) ? false : remove(o, snapshot, index);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
// 若要移除的元素为null,则直接遍历数组,找到第一个值为null的数组下标返回
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
// 遍历数组,通过equals方法找到要移除的元素的下标返回
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
// 未找到要删除的元素,默认返回-1
return -1;
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
// 1、加锁
lock.lock();
try {
// 2、获取源数组
Object[] current = getArray();
int len = current.length;
// 3、若快照与当前数组不等,则说明并发情况下源数组已经被改变
if (snapshot != current) findIndex: {
// 取较小数组长度
int prefix = Math.min(index, len);
// 遍历判断是否能能找到要删除的元素的下标,若找到则跳出if语句
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
// 若之前定位的数组下标大于当前源数组长度,则直接返回false
if (index >= len)
return false;
// 若源数组中索引下标位置的元素与要删除的元素相等,则跳出if语句
if (current[index] == o)
break findIndex;
// 遍历获取要删除的元素下标
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
// 创建新数组
Object[] newElements = new Object[len - 1];
// 将当前数组中索引下标位置之前的元素先拷贝到新数组中
System.arraycopy(current, 0, newElements, 0, index);
// 将当前数组中索引下标位置止呕的元素再拷贝到新数组中
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
// 通过setArray()方法将源数组引用指向新数组
setArray(newElements);
return true;
} finally {
// 解锁
lock.unlock();
}
}
remove()方法的工作流程其实也不复杂,顺着源码往下看就能理顺,主要流程如下:
从源码中我们就能体会到,cow容器提供了在修改操作时,采用复制新数组的方式,并在修改操作(添加或删除)中加锁,读取操作在并发情况下并不能保证读取的元素是最新的,但是修改操作会保证数据的最终一致性。
优点:
缺点:
适用场景: