- 前言
- 一、为什么要引入CopyOnWriteArrayList?
- 二、CopyOnWriteArrayList
- 总结
前言
JUC有一个常用类CopyOnWriteArrayList,非常适合在多读少写的场景,CopyOnWriteArraySet的实现也是基于 CopyOnWriteArrayList(不能存储重复数据),所以我们直接学习CopyOnWriteArrayList即可。
一、为什么要引入CopyOnWriteArrayList?
1.ArraayList 是线程不安全的(读和写都不加锁),在高并发情况下可能会出现问题。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
2.Vector是线程安全的(读和写都会加锁),但是在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修 改原有的数据,因此如果每次读取都进行加锁操作,其实是一种资源浪费。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
所以,多读写少的场景 ,JDK 中提供了CopyOnWriteArrayList类。
目的是将读取的性能发挥到极致,读不加锁,写也不会阻塞读取操作,只有写写之间互斥,读操作的性能得到大幅度提升。
二、CopyOnWriteArrayList
CopyOnWrite翻译成中文,大概就是“写数据的时候利用拷贝的副本来执行”。
CopyOnWriteArrayList类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的,其步骤如下。
1.当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。
2.写完之后,再将修改完的副本替换成原来的数据。
总结:这个过程中实际上都是在操作一个副本而已,这样就可以保证写操作不会影响读操作。
在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。
1.CopyOnWriteArrayList中add方法的实现,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
// 这个数组是核心的,因为用volatile修饰了
// 只要把最新的数组对他赋值,其他线程立马可以看到最新的数组,保证了可见性
private transient volatile Object[] array;
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;
// 然后把副本数组赋值给volatile修饰的变量
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
2.读的时候不需要加锁,直接读取,将读取的性能发挥到极致。
public E get(int index) {
return get(getArray(), index);
}
注意:如果读的时候有线程正在向CopyOnWriteArrayList添加数据。读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
总结
如果用了CopyOnWriteArrayList,就是用空间换时间,更新的时候基于副本更新,然后最后用volatile变量来赋值保证可见性,更新的时候对读线程没有任何的影响。写操作过多的情况下不能使用,因为写操作的时候牵扯到数据拷贝的情况,实际上是很耗费资源。



