在jave.util.concurrent包下有这样两个类:CopyOnWriteArrayList和CopyOnWriteArraySet。
其中利用到了CopyOnWrite机制,本篇就来聊聊CopyOnWrite技术与Java中的CopyOnWrite容器。
主要包扩以下内容:
什么是CopyOnWrite
CopyOnWriteArrayList
CopyOnWriteArraySet
CopyOnWrite适用场景
什么是CopyOnWrite对于一般的容器,比如ArrayList,在进行并发操作时,如果一个线程读,一个线程写,会抛出java.util.ConcurrentModificationException异常。而CopyOnWrite容器则避免了这种情况。
CopyOnWrite,顾名思义,写时复制,在修改集合中数据的时候,不直接修改当前容器,而是先将当前容器进行拷贝,复制出一个新的容器,然后在新的容器里完成修改,再将原容器的引用指向新的容器。
这样做的好处是,可以不通过加锁,实现对CopyOnWrite容器的并发读写。需要注意的是,CopyOnWrite技术并不保证实时一致性,因为在读写并行时,有可能会读到过期的数据。CopyOnWrite技术保证的是最终一致性。
CopyOnWriteArrayList的底层是通过数组来实现的,其包含两个属性:
12
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
前者用于在对CopyOnWriteArrayList进行修改时加锁,后者用于保存容器中的元素(允许null元素),对array加了volatile关键字,保证每次修改容器的时候对其他线程都是可见的。
在其各种接口的实现中,用的最多的是如下两个方法:
2
3
4
5
6
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
getArray()方法返回当前数组,而setArray()方法用于在CopyOnWriteArrayList变化时,将array执行修改后的数组内存地址。
CopyOnWriteArrayList提供了三种构造函数:
2
3
CopyOnWriteArrayList(); // 创建一个array长度为0的CopyOnWriteArrayList
CopyOnWriteArrayList(Collection<? extends E> c); // 以一个特定容器为参数创建CopyOnWriteArrayList
CopyOnWriteArrayList(E[] toCopyIn); // 以一个数组为参数创建CopyOnWriteArrayList
根据实际的需要创建即可。
CopyOnWriteArrayList提供的读方法与数组的读方法并无什么大的不同,因为CopyOnWriteArrayList本身解决的问题就不是读读并发的问题,所以重一下其写方法。
首先看一下set()方法,set()方法为指定位置设置特定值,如下是其实现:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1
try {
Object[] elements = getArray(); // 2
E oldValue = get(elements, index);
if (oldValue != element) { // 3
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len); // 4
newElements[index] = element; // 5
setArray(newElements); // 6
} else {
setArray(elements);
}
return oldValue;
} finally {
lock.unlock(); // 7
}
}
分别看一下以上代码中的关键几步:
可以看到CopyOnWriteArrayList为了保证在复制原容器时是加了一个可重入锁的,在set完成后释放该锁;
获取当前的数组;
判断要设置的位置的旧值与新值是否相同,如果相同则免去容器的拷贝工作;
将原容器复制一份;
修改该处的值为新值;
重新创建CopyOnWriteArrayList容器,将旧容器的内存地址改为新容器所在内存地址;
完成set,释放锁。
再看一下add()方法,向容器中添加一个元素,如下是其源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1
try {
Object[] elements = getArray(); // 2
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 3
newElements[len] = e; // 4
setArray(newElements); // 5
return true;
} finally {
lock.unlock();
}
}