什么是COW

CopyOnWrite简称COW,顾名思义,就是写时复制,即当我们要对一个对象进行修改时,先复制一份该对象的拷贝,在此拷贝上进行修改,最后将修改后的对象写回。这样的做法的好处是我们可以对对象并发读而不需要加锁。

Java中的COW容器

JDK为我们提供了两种COW容器,分别是CopyOnWriteArrayListCopyOnWriteArraySet,下面简单分析一下CopyOnWriteArrayList的源码实现(基于JDK1.8.0_66
CopyOnWriteArrayList中主要的两个成员变量

1
2
3
final transient ReentrantLock lock = new ReentrantLock();

private transient volatile Object[] array;

lock用来对写操作进行加锁;
array是实际存储数据的容器,并用volatile修饰保证线程间的可见性。

直接来看add方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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();
}
}

final void setArray(Object[] a) {
array = a;
}

final Object[] getArray() {
return array;
}

对于其他的几个修改CopyOnWriteArrayList的方法,都是大同小异,先复制再修改,最后指向新数组。

再看get方法

1
2
3
4
5
6
7
public E get(int index) {
return get(getArray(), index);
}

private E get(Object[] a, int index) {
return (E) a[index];
}

很简单,读取的时候不需要加锁。

COW容器的优缺点

  • 优点
    不加锁的读,一定程度上提高了性能。
  • 缺点
  1. 内存消耗大。这种朴素的牺牲空间解决并发问题的思想,使得每次修改都需要拷贝一份,因此需要消耗更多的内存。如果容器中的数据量很大,那么拷贝会占用大量的内存空间,可能引发GC,影响系统的性能。
  2. 无法保证数据的实时一致性。由于复制数组、修改数据需要花费时间,那么在原数组的引用还没有指向修改完成的数组之前,读到的数据就不是最新,COW容器只能保证数据的最终一致性。

总结

COW容器的特点是拷贝数据,读写分离解决并发问题、不保证数据实时一致性仅保证最终一致性。
但由于COW容器的缺点,在我看来没有很合适的应用场景,在并发的场景中使用ConcurrentHashMap一类的容器会更加合适。