1 概述
JDK1.5的j.u.c库中提供了Copy-On-Write机制,简称为COW。其实现的基本思想是,开始的时候只有一份内容,当某个线程想要修改这个内容的时候,会将其拷贝出去形成一份新的内容,在这份新的内容上进行修改,这是一种延时懒惰策略。用该机制形成的有两个并发容器,即CopyOnWriteArrayList和CopyOnWriteArraySet。其中,后者是基于前者实现的,当进行数据的添加和删除时是在数组上进行遍历的,这和HashSet的哈希表实现以及TreeSet的二叉树实现是不同的,所以其效率是比较低效的,只是实现了元素的唯一性。
这里主要讨论一下CopyOnWriteArrayList的实现原理。
2 CopyOnWriteArrayList性能测试
为了测试CopyOnWriteArrayList的性能,这里将普通的ArrayList与其一起进行测试。测试的过程是,同时开启10个写线程和10个读线程对同一个list进行操作,具体的测试代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by fubinhe on 16/12/1.
*/
public class CopyOnWriteArrayListDemo {
public static final int NUM = 10;
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < NUM; ++i) {
list.add("main_" + i);
}
list.get(0);
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < NUM; ++i) {
exec.execute(new WriteTask(list, i));
exec.execute(new ReadTask(list));
}
exec.shutdown();
}
}
class WriteTask implements Runnable {
private List<String> list;
private int removeIndex;
public WriteTask(List<String> list, int removeIndex) {
this.list = list;
this.removeIndex = removeIndex;
}
@Override
public void run() {
list.remove(removeIndex);
list.add("write_" + removeIndex);
}
}
class ReadTask implements Runnable {
private List<String> list;
public ReadTask(List<String> list) {
this.list = list;
}
@Override
public void run() {
for (String s : list) {
System.out.println(s);
}
}
}
运行上述代码,程序将抛出ConcurrentModificationException异常。这是因为,在读线程试图读取下一个元素的时候,该元素恰好被写线程删除了,迭代器失效。如果将第16行代码替换成下面的代码,则可以顺利的执行并发的读写。
List<String> list = new CopyOnWriteArrayList<>();
3 源代码分析
CopyOnWriteArrayList之所以能进行并发的读写,是因为采用了读写分离的思想,读和写是在不用的容器中进行的。下面分析一些其核心方法的源代码。
3.1 add方法
add方法的源代码如下,读起来也比较好懂。在开始写之前加锁,然后将原有的数组复制出来,在新数组的结尾加上一个新元素后,将新数组的引用赋给原有的数组引用,最后释放持有的锁。
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();
}
}
3.2 remove方法
和普通ArrayList类似,remove方法有两种形式,即删除某个索引的值和删除某个对象,这里讨论一下第一种形式,其源代码如下:
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();
}
}
和add方法类似,其实现方式也比较简单,以某个索引为界分两次将原数组的值拷贝到新数组中。
3.3 get方法
CopyOnWriteArrayList在执行读操作时不进行加锁,所以其实现更简单,直接返回数组中某个索引的值。由于没有加锁,所以如果在读的同时有写线程对数组进行操作,那么有可能读到的值并不是最新的。
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
4 总结
COW机制使用读写分离的思想提高并发性能,使得多个读写操作能同时进行。但是很明显,其也有两个缺点,即:
(1)由于读操作没有加锁,所以如果在读的过程中也有写操作在进行,那么无法保证实时的一致性,只能做到最终的一致性。
(2)占用内存多。因为在对数组进行修改时,需要将原有的数组拷贝到新的数组中,那么内存中会多维护一份数组。如果修改的操作频繁,会造成比正常更多的FULL GC。