回答
CopyOnWriteArrayList
是一个线程安全的 ArrayList,其核心思想是“写时复制”,即每当我们对列表进行修改(添加、删除、更新元素)时,它都会先创建该列表的一个副本,然后在这个副本上进行修改。修改完成后,它会将原来的列表引用指向新修改过的副本。这种机制有效减少了线程之间的竞争,因为读操作可以安全地访问列表,而不需要担心写操作同时发生的情况。
copy-on-Write 体现的是一种读写分离的思想。
CopyOnWriteArrayList 实现原理
原理
写时复制原理图:
CopyOnWriteArrayList
的实现原理比较简单,源码也很简单,我们大致看下就可以了。
- 结构
CopyOnWriteArrayList
使用一个 volatile 类型的数组来存储数据,volatile 保证这个数组引用的可见性,当数组发生变化时,其他所有线程都能立即看到这个变化。
private transient volatile Object[] array;
同时为了保证线程安全,CopyOnWriteArrayList
新建了一个 Object 锁对象。
final transient Object lock = new Object();
- 添加元素
当我们需要添加一个元素时,CopyOnWriteArrayList
会先锁定整个类(利用 object 锁对象),然后复制一个新的数组,大小为原数组大小加 1,然后将新元素添加到新数组的末尾,最后再将引用指向新的数组。
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
这里为什么只 + 1 呢?因为 CopyOnWriteArrayList
的使用场景是读多写少的场景,旨在优化读操作,选择牺牲了写的性能。只 + 1 的原因是为了减少内存占用和复制成本。如果每次增加的空间过多,将导致不必要的内存浪费,特别是在元素添加不频繁的情况下。有小伙伴说,多加点又如何呢?多加点说明你写的场景有点儿多,这个时候你要考虑的就不是 CopyOnWriteArrayList
了。
- 读取元素
CopyOnWriteArrayList
旨在优化读操作,所以读取是无锁的。由于所有的修改操作都会创建一个新的数组,所以不会出现并发问题。
public E get(int index) {
return elementAt(getArray(), index);
}
缺点
- 内存占用和复制开销:
CopyOnWriteArrayList
在每次修改时都需要复制整个列表,这可能会导致较高的内存占用和复制开销。因此,如果列表较大或者写操作频繁,CopyOnWriteArrayList
可能不是好的选择。 - 数据一致性问题::
CopyOnWriteArrayList
提供的迭代器是弱一致性的。这意味着在遍历元素时是基于列表的某个固定版本进行的,即使后续有修改操作,迭代器看到的内容也不会改变。