CopyOnWriteArrayList源码阅读
CopyOnWriteArrayList实现了List,RandomAccess,Cloneable,Serializable接口
CopyOnWrite容器即写时复制容器。通俗地讲,当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器复制出一个新的容器,然后在新的容器里添加元素,添加玩元素之后再讲原来容器的引用指向新的容器。这样可以做到对COW容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以COW容器也是一种读写分离的思想,读和写不同的容器。COW是一种程序设计中的优化策略,是一种延时懒惰策略。从JDK.15以后juc包提供了两个使用COW机制实现的并发容器,分别是CopyOnWriteArrayList和CopyOnWriteArraySet(CopyOnWriteArraySet内部持有一个CopyOnWriteArrayList,很多方法都是调用CopyOnWriteArrayList来实现的)。这样在任何时候我们都可以并发地进行读操作。
属性分析
/** The lock protecting all mutators */
transient final ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
这里有两个比较重要的知识点——volatile和ReentrantLock,具体原理及使用参见上节
方法分析
构造函数
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
setArray(elements);
}
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
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();
}
}
可以看到add方法与ArrayList方法明显的两个区别在于ReentrantLock重入锁的使用以及复制副本
另外一个在指定索引位置插入元素的方法如下:
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock(); //加锁
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0) //插入到最后
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index); //复制index前的数组
System.arraycopy(elements, index, newElements, index + 1, //复制index后的数组,index位置空出
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
get and set
//get方法没有加锁,可以并行进行读操作
public E get(int index) {
return get(getArray(), index);
}
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();
}
}
注意到else中有一行代码,正如注释所说,这行代码是为了维护volatile语义的。线程中将一个对象放入任何并发collection之前的操作 happen-before 从另一线程中的collection访问或移除该元素的后续操作。
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); //复制index前数组
System.arraycopy(elements, index + 1, newElements, index, numMoved); //复制index后数组
setArray(newElements); //使用set方法保证volatile可见性
}
return oldValue;
} finally {
lock.unlock();
}
}
应用
COWb并发容器用于读多写少的并发场景,比如白名单,黑名单。商品类目的访问和更新场景。
COW存在两个问题,内存占用和数据一致性
- 内存占用
因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。
- 数据一致性问题
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,则不能使用CopyOnWrite容器。