CopyOnWriteArrayList 简介
CopyOnWriteArrayList是一种线程安全的ArrayList,底层是基于数组实现。
Copy-On-Write 的思想是什么
"Copy-On-Write"即写时复制:需要修改( add
,set
、remove
等操作) CopyOnWriteArrayList
的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
缺点:
-
内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。
-
写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。
-
数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。
CopyOnWriteArrayList 源码分析
-
List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 -
RandomAccess
:这是一个标志接口,表明实现这个接口的List
集合是支持 快速随机访问 的。 -
Cloneable
:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 -
Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
初始化
CopyOnWriteArrayList
中有一个无参构造函数和两个有参构造函数。
scss
// 创建一个空的 CopyOnWriteArrayList
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 按照集合的迭代器返回的顺序创建一个包含指定集合元素的 CopyOnWriteArrayList
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
// 创建一个包含指定数组的副本的列表
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
插入元素
CopyOnWriteArrayList
的 add()
方法有三个版本:
add(E e)
:在CopyOnWriteArrayList
的尾部插入元素。add(int index, E element)
:在CopyOnWriteArrayList
的指定位置插入元素。addIfAbsent(E e)
:如果指定元素不存在,那么添加该元素。如果成功添加元素则返回 true。
依add(E e)为例:
csharp
// 插入元素到 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;
// array指向新数组
setArray(newElements);
return true;
} finally {
// 解锁
lock.unlock();
}
}
- 先使用ReentrantLock加锁,保证线程安全。
- 再创建一个新数组,长度是原数组长度+1,并把原数组元素拷贝到新数组里面。
- 然后在新数组末尾位置赋值
- 使用新数组替换掉原数组
- 最后释放锁
读取元素
CopyOnWriteArrayList
的读取操作是基于内部数组 array
并没有发生实际的修改,因此在读取操作时不需要进行同步控制和锁操作,可以保证数据的安全性。这种机制下,多个线程可以同时读取列表中的元素。
typescript
// 底层数组,只能通过getArray和setArray方法访问
private transient volatile Object[] array;
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
方法是弱一致性的,在某些情况下可能读到旧的元素值。
get(int index)
方法是分两步进行的:
- 通过
getArray()
获取当前数组的引用; - 直接从数组中获取下标为 index 的元素。
这个过程并没有加锁,所以在并发环境下可能出现如下情况:
- 线程 1 调用
get(int index)
方法获取值,内部通过getArray()
方法获取到了 array 属性值; - 线程 2 调用
CopyOnWriteArrayList
的add
、set
、remove
等修改方法时,内部通过setArray
方法修改了array
属性的值; - 线程 1 还是从旧的
array
数组中取值。
获取列表中元素的个数
csharp
public int size() {
return getArray().length;
}
CopyOnWriteArrayList
中的array
数组每次复制都刚好能够容纳下所有元素,并不像ArrayList
那样会预留一定的空间。因此,CopyOnWriteArrayList
中并没有size
属性CopyOnWriteArrayList
的底层数组的长度就是元素个数,因此size()
方法只要返回数组长度就可以了
删除元素
CopyOnWriteArrayList
删除元素相关的方法一共有 4 个:
remove(int index)
:移除此列表中指定位置上的元素。将任何后续元素向左移动(从它们的索引中减去 1)。boolean remove(Object o)
:删除此列表中首次出现的指定元素,如果不存在该元素则返回 false。boolean removeAll(Collection<?> c)
:从此列表中删除指定集合中包含的所有元素。void clear()
:移除此列表中的所有元素。
这里以remove(int index)
为例进行介绍:
ini
public E remove(int index) {
// 获取可重入锁
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
//获取当前array数组
Object[] elements = getArray();
// 获取当前array长度
int len = elements.length;
//获取指定索引的元素(旧值)
E oldValue = get(elements, index);
int numMoved = len - index - 1;
// 判断删除的是否是最后一个元素
if (numMoved == 0)
// 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组
setArray(Arrays.copyOf(elements, len - 1));
else {
// 分段复制,将index前的元素和index+1后的元素复制到新数组
// 新数组长度为旧数组长度-1
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//将新数组赋值给array引用
setArray(newElements);
}
return oldValue;
} finally {
// 解锁
lock.unlock();
}
}
- 先使用ReentrantLock加锁,保证线程安全。
- 再创建一个新数组,长度是原数组长度-1,并把原数组中剩余元素(不包含需要删除的元素)拷贝到新数组里面。
- 使用新数组替换掉原数组
- 最后释放锁
判断元素是否存在
CopyOnWriteArrayList
提供了两个用于判断指定元素是否在列表中的方法:
contains(Object o)
:判断是否包含指定元素。containsAll(Collection<?> c)
:判断是否保证指定集合的全部元素。
typescript
// 判断是否包含指定元素
public boolean contains(Object o) {
//获取当前array数组
Object[] elements = getArray();
//调用index尝试查找指定元素,如果返回值大于等于0,则返回true,否则返回false
return indexOf(o, elements, 0, elements.length) >= 0;
}
// 判断是否保证指定集合的全部元素
public boolean containsAll(Collection<?> c) {
//获取当前array数组
Object[] elements = getArray();
//获取数组长度
int len = elements.length;
//遍历指定集合
for (Object e : c) {
//循环调用indexOf方法判断,只要有一个没有包含就直接返回false
if (indexOf(e, elements, 0, len) < 0)
return false;
}
//最后表示全部包含或者制定集合为空集合,那么返回true
return true;
}
参考文章:
1.CopyOnWriteArrayList 源码分析 | JavaGuide(Java面试 + 学习指南) 2.阿里Java面试官:CopyOnWriteArrayList底层是怎么保证线程安全的? - 掘金 (juejin.cn)