引言
在 Java 并发编程领域,对于线程安全的列表需求颇为常见。CopyOnWriteArrayList
作为 java.util.concurrent
包下的一员,为我们提供了一种独特的线程安全列表实现方式。它特别适用于读多写少的场景,能在保证线程安全的同时,尽量减少对读操作的性能影响。本文将深入探究 CopyOnWriteArrayList
的原理,涵盖其底层数据结构、核心属性、构造方法、常用操作的实现细节、性能特点以及应用场景。
1. CopyOnWriteArrayList 概述
1.1 定义与用途
CopyOnWriteArrayList
是 Java 中实现了 List
接口的线程安全列表。"CopyOnWrite" 即写时复制,这意味着在进行写操作(如添加、删除、修改元素)时,它会创建一个原数组的副本,在副本上进行修改,修改完成后再将引用指向新的数组。这种机制使得读操作无需加锁,从而提高了读操作的性能,尤其适用于读操作远远多于写操作的场景。
1.2 继承关系与实现接口
CopyOnWriteArrayList
实现了 List
接口,这表明它具备列表的基本特性,如可以按索引访问元素、支持元素的添加和删除等。同时,它还实现了 RandomAccess
接口,支持随机访问,以及 Cloneable
和 java.io.Serializable
接口,允许进行克隆和序列化操作。
java
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListOverview {
public static void main(String[] args) {
// 创建一个 CopyOnWriteArrayList 对象
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 可以将其赋值给 List 接口类型的变量
List<String> listInterface = list;
}
}
2. 底层数据结构:可复制的数组
2.1 基本概念
CopyOnWriteArrayList
底层基于数组实现。与普通数组不同的是,在进行写操作时,它会创建一个新的数组副本,将原数组中的元素复制到新数组中,然后在新数组上进行修改,最后将引用指向新数组。这样,读操作可以在原数组上进行,无需加锁,从而提高了读操作的并发性能。
2.2 核心属性
CopyOnWriteArrayList
有一个核心属性用于存储数组:
java
private transient volatile Object[] array;
array
:用于存储列表中的元素。volatile
关键字保证了该数组的可见性,即一个线程对数组的修改能够及时被其他线程看到。
3. 构造方法
3.1 无参构造方法
java
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
无参构造方法创建一个空的 CopyOnWriteArrayList
,初始数组为空。
3.2 带集合参数的构造方法
java
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);
}
该构造方法接受一个集合作为参数,将集合中的元素复制到 CopyOnWriteArrayList
中。如果传入的集合也是 CopyOnWriteArrayList
类型,则直接获取其底层数组;否则,将集合转换为数组并进行复制。
3.3 带数组参数的构造方法
java
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
此构造方法接受一个数组作为参数,将数组中的元素复制到 CopyOnWriteArrayList
中。
java
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListConstructors {
public static void main(String[] args) {
// 无参构造方法
CopyOnWriteArrayList<String> list1 = new CopyOnWriteArrayList<>();
// 带集合参数的构造方法
Collection<String> collection = new ArrayList<>();
collection.add("apple");
collection.add("banana");
CopyOnWriteArrayList<String> list2 = new CopyOnWriteArrayList<>(collection);
// 带数组参数的构造方法
String[] array = {"cherry", "date"};
CopyOnWriteArrayList<String> list3 = new CopyOnWriteArrayList<>(array);
System.out.println(list2);
System.out.println(list3);
}
}
4. 常用操作原理
4.1 添加元素
java
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;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
add(E e)
方法使用ReentrantLock
进行加锁,确保写操作的线程安全。- 首先获取原数组,然后创建一个长度比原数组大 1 的新数组。
- 将原数组中的元素复制到新数组中,并将新元素添加到新数组的末尾。
- 最后将引用指向新数组,并释放锁。
4.2 访问元素
java
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
get(int index)
方法直接从数组中获取指定索引的元素,无需加锁。这是因为读操作是在原数组上进行的,不会受到写操作创建新数组的影响。
4.3 修改元素
java
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
set(int index, E element)
方法使用ReentrantLock
进行加锁。- 首先获取原数组,并获取指定索引的旧元素。
- 如果新元素与旧元素不同,则创建一个新数组,将原数组中的元素复制到新数组中,并修改指定索引的元素。
- 最后将引用指向新数组,并释放锁。
4.4 删除元素
java
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();
}
}
remove(int index)
方法使用ReentrantLock
进行加锁。- 首先获取原数组,并获取指定索引的元素。
- 如果删除的是数组的最后一个元素,则直接创建一个长度比原数组小 1 的新数组。
- 否则,创建一个新数组,将原数组中指定索引之前和之后的元素复制到新数组中。
- 最后将引用指向新数组,并释放锁。
5. 迭代器原理
CopyOnWriteArrayList
的迭代器是弱一致性的。当创建迭代器时,它会保存当前数组的快照,迭代器的操作是基于这个快照进行的,不会受到后续写操作的影响。
java
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListIterator {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("apple");
list.add("banana");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// 在迭代过程中进行写操作
list.add("cherry");
// 再次迭代,使用新的迭代器
Iterator<String> newIterator = list.iterator();
while (newIterator.hasNext()) {
System.out.println(newIterator.next());
}
}
}
6. 性能分析
6.1 时间复杂度
- 读操作 :读操作(如
get
方法)的时间复杂度为 O(1),因为直接从数组中获取元素,无需加锁。 - 写操作 :写操作(如
add
、set
、remove
方法)的时间复杂度为 O(n),因为需要创建新数组并复制元素。
6.2 空间复杂度
CopyOnWriteArrayList
的空间复杂度较高,因为每次写操作都会创建一个新数组,会占用额外的内存空间。
7. 应用场景
7.1 读多写少的场景
由于 CopyOnWriteArrayList
的读操作无需加锁,性能较高,因此适用于读操作远远多于写操作的场景,如配置信息存储、缓存等。
7.2 对数据实时性要求不高的场景
由于迭代器是弱一致性的,在迭代过程中可能看不到最新的数据更新,因此适用于对数据实时性要求不高的场景。
8. 总结
CopyOnWriteArrayList
通过写时复制的机制,在保证线程安全的同时,提高了读操作的并发性能。它适用于读多写少、对数据实时性要求不高的场景。然而,由于每次写操作都会创建新数组,会带来较高的空间开销和写操作性能开销。在使用时,需要根据具体的业务场景权衡其优缺点,合理选择数据结构。