一、CopyOnWriteArrayList介绍
①、CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
②、CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。
③、CopyonWriteArrayList 合适读多写少的场景,不过这类慎用 ,因为谁也没法保证CopyonWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
构造函数:
private transient volatile Object[] array;//通过volatile保证可见性,但是其实也影响一点性能。因为每次都需要写回主内存之后,再从主内存强制加载到工作内存。
public CopyOnWriteArrayList() {
// 创建了一个大小为0的Object数据作为array的初始值
setArray(new Object[0]);
}
Add方法:
final transient ReentrantLock lock = new ReentrantLock();
public boolean add(E e) {
//加独占锁(1)
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取array(2)
Object[] elements = getArray();
//拷贝array到新数组,添加元素到新数组(3)
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//使用新数组替换添加前的数组(4)
setArray(newElements);
return true;
} finally {
//释放独占锁(5)
lock.unlock();
}
}
Get方法:
public E get(int index) {
//获取到当前list里面的array数组,步骤1
return get(getArray(), index);
}
//当执行完步骤1后执行步骤2前,另外一个线程C进行了修改操作,比如remove操作(修改当前list的array为新数组),
则步骤2操作的是线程C删除元素前的一个快照数组(因为步骤1让array指向的是原来的数组),这就是写时拷贝策略带来弱一致性.
private E get(Object[] a, int index) {
//通过随机访问的下标方式访问指定位置的元素,步骤2。
return (E) a[index];
}
Set方法
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();
}
}
Remove方法
public E remove(int index) {
//获取独占锁保证删除数组期间,其他线程不能对array进行修改
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();
}
}
迭代器的弱一致性:
private static volatile CopyOnWriteArrayListarrayList = new CopyOnWriteArrayList<>(); @SneakyThrows public static void main(String[] args) { arrayList.add("hello"); arrayList.add("Java"); arrayList.add("welcome"); arrayList.add("to"); arrayList.add("hangzhou"); Thread thread = new Thread(new Runnable() { @Override public void run() { //修改list中下标为1的元素为JavaSe arrayList.set(1, "JavaSe"); //删除元素 arrayList.remove(2); arrayList.remove(3); } }); //保证在修改线程启动前获取迭代器 Iterator itr = arrayList.iterator(); //启动线程 thread.start(); //等在子线程执行完毕 thread.join(); //迭代元素 while (itr.hasNext()) { System.out.println(itr.next()); } }
结果如下:
CopyOnWriteArrayList中迭代器是弱一致性,所谓弱一致性是指返回迭代器后,其他线程对list的增删改对迭代器不可见,无感知的.下面就看看是如何做到的:
public Iteratoriterator() { return new COWIterator (getArray(), 0); } static final class COWIterator implements ListIterator { //array的快照版本 private final Object[] snapshot; //数组下标 private int cursor; //构造函数 private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } //是否遍历结束 public boolean hasNext() { return cursor < snapshot.length; } //获取元素 public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; }}
二、CopyonWriteArrayList 有几个缺点:
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc。
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyonWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求,如:;
public static void main(String[] args) {
List list = new CopyOnWriteArrayList<>();
for(int i = 0; i<10000; i++){
list.add("string" + i);
}
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (list.size() > 0) {
String content = list.get(list.size() - 1);
}else {
break;
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if(list.size() <= 0){
break;
}
list.remove(0);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 9963 at java.util.concurrent.CopyOnWriteArrayList.get(CopyOnWriteArrayList.java:388) at java.util.concurrent.CopyOnWriteArrayList.get(CopyOnWriteArrayList.java:397) at com.example.demo.controller.Test01Cont$1.run(Test01Cont.java:64) at java.lang.Thread.run(Thread.java:748)
从上可以看出CopyOnWriteArrayList并不是完全意义上的线程安全,如果涉及到remove操作,一定要谨慎处理。



