CopyOnWriteArrayList 实现原理
Posted 盛夏温暖流年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CopyOnWriteArrayList 实现原理相关的知识,希望对你有一定的参考价值。
CopyOnWriteArrayList 是什么
CopyOnWriteArrayList 是 Java 并发包 java.util.concurrent 中提供的并发容器,本质上是一个线程安全且读操作无锁的 ArrayList。它在确保线程安全的前提下,通过牺牲写操作的效率来保证读操作的高效。
所谓 CopyOnWrite 就是通过复制的方式来完成对数据的修改,在修改时复制一个新的数组,在上面进行修改,不会对旧的数组进行改变,也就没有读写数据不一致的问题了。
CopyOnWriteArrayList 优缺点
优点
- 读操作性能高
因为无需任何同步措施,比较适用于读多写少的并发场景。
- 迭代器遍历不会抛出异常
遍历 ArrayList 时,若中途有别的线程对其修改,则会抛出 ConcurrentModificationException 异常。而 CopyOnWriteArrayList 由于其"读写分离"的思想,它的遍历和修改操作分别作用在不同的 list 容器上,所以在使用迭代器遍历时,不会抛出 ConcurrentModificationException 异常。
缺点
- 内存占用大
每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;
- 无法保证实时性
CopyOnWriteArrayList 的写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞,所以读取到的是老容器的数据,无法保证数据的实时性。
CopyOnWriteArrayList 实现原理
如图所示,CopyOnWriteArrayList 容器允许并发读,读操作是无锁的,性能较高。
而进行写操作时,需要首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器,相对来说性能较差。
CopyOnWriteArrayList 的关键数据结构如下:
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
可以看到,CopyOnWriteArrayList 底层实现和 ArrayList 一样,用数组来保存元素,但它多了把独占锁 lock,来保证线程安全。
CopyOnWriteArrayList 关键方法分析
add 方法
public boolean add(E e) {
//ReentrantLock加锁,保证线程安全
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();
}
}
添加方法的逻辑很简单:
- 使用 ReentrantLock 加锁,保证线程安全;
- 对原始容器进行拷贝得到副本;
- 在新的副本上执行添加操作,添加完成后,将原始容器的引用指向新的副本;
- 执行解锁操作;
remove 方法
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();
}
}
代码逻辑如下:
- 加锁,保证多线程下的数据安全
- 获取到原始数组并定位到需要删除的 index 索引下标
- 如果要删除的是列表末端数据,拷贝前 len-1 个数据到新副本上,再切换引用;
- 否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用;
- 执行解锁操作;
get 方法
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
可以看到,它的 get 方法分为2步,先获取数组,再获取 index 位置的元素,这2步都是没有加锁的?为什么不需要加锁呢?
上面提到,add() 是先拷贝原数组,然后在拷贝的数组上操作的,在 setArray() 之前对原数组并没有影响,因此读的时候不需要加锁。虽然不需要加锁,但会出现数据弱一致性问题,也就是说 CopyOnWriteArrayList 只能保证数据的最终一致性。
参考博客如下,非常感谢:
以上是关于CopyOnWriteArrayList 实现原理的主要内容,如果未能解决你的问题,请参考以下文章