Copy-On-Write
简称COW
,是一种用于集合的并发访问的优化策略。
基本思想是:当我们往一个集合容器中写入元素时(添加、修改、删除),并不会直接在集合容器中写入,而是先将当前集合容器进行Copy,复制出一个新的容器,然后新的容器里写入元素,写入操作完成之后,再将原容器的引用指向新的容器。
这样做的好处:实现对CopyOnWrite
集合容器写入操作时的线程安全,但同时并不影响进行并发的读取操作。所以CopyOnWrite
容器也是一种读写分离的思想。从JDK1.5
开始Java
并发包里提供了两个使用CopyOnWrite
机制实现的并发集合容器,它们是CopyOnWriteArrayList
和CopyOnWriteArraySet
。
CopyOnWriteArrayList
相当于线程安全的ArrayList
,内部存储结构采用Object[]
数组,线程安全使用ReentrantLock
实现,允许多个线程并发读取,但只能有一个线程写入。
接下来我们一起来看看CopyOnWriteArrayList是如何实现写入时保证线程安全的。
add()方法:
当添加新元素到集合时,将原数组中元素copy到一个新数组中,再将新添加的元素放入新数组中,最后将用新数组替换原数组。在此期间使用ReentrantLock
加锁,保证线程安全,避免多个线程复制数组。
通过观察源码也可以得出结论:
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();
}
}
set()方法:
修改集合中元素时,首先我们得先获取到原数组,再获取原数组中要修改的元素。接下来判断修改的值和原数组中的值是否相等。不相等则copy原数组元素到一个新数组中,再修改新数组中的值,修改完成,将新数组替换成原数组。相等返回原数组。
修改源码如下:
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;
//复制原数组中的值到新数组
Object[] newElements = Arrays.copyOf(elements, len);
//将对应位置的值,修改为新值
newElements[index] = element;
//用新数组替换原来数组
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
//相等则返回原数组
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
remove()方法:
删除集合中的指定下标-元素时,获取原数组,判断删除的元素是否是最后一个,如果是则copy原数组除最后一个元素外所有元素,替换原数组。不是则创建一个新数组copy原数组中除当前元素的所有元素,用新数组替换原数组。这样就完成了删除操作。
源码分析:
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();
}
}
get()方法
根据指定下标,到原数组中读取元素。读取过程中不加锁,允许多个线程并发读取。但是如果读取的时候,有其它线程向集合中添加新元素,此时仍然读取到的是旧数据。因为添加操作没有对原数组加锁。
源码分析:
public E get(int index) {
// 根据指定下标,从原数组中读取元素
return get(getArray(), index);
}private E get(Object[] a, int index) {
return (E) a[index];
}
CopyOnWrite
容器是一种读写分离的思想,CopyOnWriteArrayList
读操作时候不加锁而只对写入操作加锁,虽然保证了线程安全,但也影响了写入的效率。所以CopyOnWriteArrayList在读多写少的场合性能很好。