工作中遇到CopyOnWriteArrayList这个类,略好奇,简单学习了一下,这里做个笔记。
一、概念
CopyOnWrite容器是在JDK 1.5 的java.uti.concurrent包中出现的,目前只有两个类
- CopyOnWriteArrayList
- CopyOnWriteSet
这些容器类与平时用到的ArrayList/Set的区别之处在于:
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
二、源码分析
与ArrayList类似,CopyOnWriteArrayList内部同样是基于数组实现,如下:
/** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array;
array数组被transient关键字修饰,保证该数组不能使用Serializable接口自动序列化;被volatile修饰,保证了多线程访问时的可见性,即保证每次读取的是最新的值,但是没保证对变量的操作的原子性(有关volatile关键的分析,参见:Java并发编程:volatile关键字解析)。与ArrayList相比,CopyOnWriteArrayList多出了volatile关键字。
而CopyOnWriteArrayList区别于ArrayList的主要地方在于add/set/remove等涉及到写数据的方法:
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();
}
}
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();
}
}
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();
}
}
ArrayList在add/set/remove时,只需要考虑内部的数组就可以,不用考虑并发。而CopyOnWriteArrayList则采用可重入锁,避免多线程写操作,并且写的时候是重新拷贝一个新的数组,等拷贝结束、将修改引用后再释放锁。因此,CopyOnWriteArrayList若保存对象较大、占用内存较多时可能造成JVM频繁进行gc。另外,CopyOnWriteArrayList只保证了数据的最终一致性,并没有保证数据的实时一致性。
三、场景应用常见
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景