背景
我们知道,多个线程操作ArrayList的时候,会抛出异常:
ArrayList中有一个failfast机制。比如一开始确定集合长度,同时会决定遍历的次数。但是如果某个线程读取的时候,有其他线程又插入元素,就破坏了一开始读到的快照,然后判断之前读到的快照和现在数组不同,就会直接抛出异常。
所以Java中增加了线程安全的List:copyOnWriteArrayList.
copyOnWriteArrayList可以保证多个线程同时读写是线程安全的。
我们自己实现的话,可能会考虑加reentrantLock。但是这样锁的粒度太大了,性能较低。
v1.0:还有一个中思路是使用ReentrantReadWriteLockl. 写的时候lock.writelock(), 读读不互斥,读写互斥, 写写互斥。
这个方案在读写都比较大的场景下是可以的。
但是如果在读多写少的情况下,使用读写锁效率不是很高。
这个时候就可以考虑使用CopyOnWrite机制。
核心思想
读写分离,空间换时间,避免为保证并发安全导致的激烈的锁竞争。
划关键点:
1、CopyOnWrite适用于读多写少的情况,最大程度的提高读的效率;
2、CopyOnWrite是最终一致性,在写的过程中,原有的读的数据是不会发生更新的,只有新的读才能读到最新数据;
3、如何使其他线程能够及时读到新的数据,需要使用volatile变量;
4、写的时候不能并发写,需要对写操作进行加锁;
5、保证数据的最终一致性,允许读到脏数据。
源码原理
写时复制
我们来看一下添加的源码:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}
----------------------------------------
final void setArray(Object[] a) {
array = a;
}