CopyOnWriteArrayList使用及实现原理
CopyOnWriteArrayList简介
CopyOnWriteArrayList是线程安全List,具有常见集合的API,当在读多写少时并且服务器内存够用的情况下非常便于使用。
CopyOnWriteArrayList使用及其场景
由于CopyOnWriteArrayList实现了List接口所以,CopyOnWriteArrayList也有常见操作集合的方法如add(),remove()。
在我做的一个项目中, 需要存放学生考试信息。刚开始没有考虑多线程的影响。就采用了如下方式:
//用于存放考试学生信息ArrayList
public static List<Map<String,Object>> sessionList = new ArrayList<>();
但每次重启服务时,都会报一个错误:ConcurrentModificationExceptionError解决办法请移步
这是由于每次项目重启,定时器就会有多个任务会执行,这时不仅有读的线程去操作学生信息ArrayList,而且还会有写的线程去操作学生信息ArrayList。
//用于存放考试学生信息CopyOnWriteArrayList
public static List<Map<String,Object>> sessionList = new CopyOnWriteArrayList<>();
所以我将ArrayList换为CopyOnWriteArrayList。之后这个问题就解决了。
CopyOnWriteArrayList实现原理
那么为什么使用CopyOnWriteArrayList就没有问题呢?那我们现在看看CopyOnWriteArrayList的源码。
public boolean add(E e) {
//获取可重入锁
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取原有数组
Object[] elements = getArray();
int len = elements.length;
//将原有数组复制一份,并将长度加1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//添加元素
newElements[len] = e;
//将原来数组对象的引用指向新的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
//将数组引用指向新的数组
array = a;
}
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
//直接获取元素
return (E) a[index];
}
看源码我们得知:每次添加元素就会复制一份数组副本在内存空间,当添加操作完成,再将数组的引用指向新的数组,这样就不存在又在读又在写的情况。(读的线程操作原始数组,写的线程操作复制的副本数组)
但这里会存在数据一致性问题:只能保证数据最终一致性问题,不能保证数据实时一致性问题。
由于读时一直在进行V1,而写随时可能进行v2,我们只会在写完后才回去改变引用指向新的对象v2。这个过程中读就是一直读的上个一个版本v1。如果实际场景不符则需要采用其他线程安全集合。
CopyOnWriteArrayList总结
CopyOnwrite适用于读极多(读的并发量很多),写极少(写的性能很低)。由于每次都是将数组重新复制一份,当一个数组里面数据量很大时,这时在复制一个副本就容易造成GC;会存在数据一致性问题:只能保证数据最终一致性问题,不能保证数据实时一致性问题。