CopyOnWriteArrayList
是 Java 中一种线程安全的 List 实现,它通过一种独特的写时复制机制(Copy-On-Write)来保证线程安全性。该类位于 java.util.concurrent
包中,适用于读操作远多于写操作的场景。在这种场景下,CopyOnWriteArrayList
可以提供比其他线程安全的集合(如 Vector
或 Collections.synchronizedList
)更高的并发读性能。
1. CopyOnWriteArrayList
概述
CopyOnWriteArrayList
是 List
接口的实现类,其底层使用一个 volatile
修饰的数组来存储元素。它的关键特性是:当对列表进行写操作(如添加、删除或修改元素)时,会创建一个底层数组的新副本,修改操作将在这个副本上进行,修改完成后,再将该副本设置为新的底层数组。这种“写时复制”的机制确保了读操作的线程安全性,因为读操作总是访问稳定的、不会被修改的数组。
1.1 线程安全性
CopyOnWriteArrayList
的线程安全性来自于以下几点:
-
写操作的独立性:每次写操作(如
add()
、remove()
、set()
等)都会创建一个底层数组的新副本,因此不会影响当前正在读取的线程。这种方式避免了使用锁机制的复杂性,也避免了锁竞争。 -
读操作的无锁化:读操作(如
get()
、iterator()
等)不需要加锁,因为底层数组在读操作期间是不可变的。由于读取操作总是访问原始数组的快照,因此不需要同步机制。
1.2 性能特点
CopyOnWriteArrayList
的性能特点如下:
-
读操作高效:由于读操作不需要加锁,因此在高并发的环境下,
CopyOnWriteArrayList
可以提供非常高效的读性能。 -
写操作开销较大:每次写操作都会创建数组的副本,这在涉及大量写操作的场景下会显著增加内存开销和垃圾回收压力。因此,它适用于读多写少的场景。
2. CopyOnWriteArrayList
的内部实现
为了更深入地理解 CopyOnWriteArrayList
,需要了解它的内部实现机制。下面是它的关键实现细节。
2.1 底层数据结构
CopyOnWriteArrayList
的底层数据结构是一个被 volatile
修饰的数组 Object[]
:
private transient volatile Object[] array;
通过 volatile
关键字修饰,确保了 array
的修改对所有线程都是可见的。
2.2 读操作实现
读操作的核心在于操作的是一个不可变的数组副本,这使得读操作非常快速和高效。典型的读操作如 get(int index)
,实现如下:
public E get(int index) {
return (E) getArray()[index];
}
getArray()
方法简单地返回当前的 array
,由于 array
是 volatile
修饰的,因此不会出现线程不可见的问题,且读操作不需要进行任何同步处理。
2.3 写操作实现
写操作则相对复杂一些,因为它涉及创建数组副本。以下是 add(E e)
方法的实现:
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();
}
}
可以看到,写操作首先获取了一个 ReentrantLock
锁来确保操作的原子性。然后:
- 调用
getArray()
获取当前的数组。 - 使用
Arrays.copyOf()
方法创建数组的新副本,并将新元素添加到副本中。 - 调用
setArray()
将新的数组副本设置为当前数组。 - 解锁并返回。
2.4 迭代器(Iterator)
CopyOnWriteArrayList
的迭代器实现与普通的 ArrayList
不同,它返回的是一个对底层数组的快照(snapshot),这意味着迭代器创建之后的任何修改操作(如 add()
、remove()
等)都不会影响迭代器。以下是 CopyOnWriteArrayList
迭代器的实现方式:
public Iterator<E> iterator() {
return new COWIterator<>(getArray(), 0);
}
COWIterator
是一个内部类,它的实现方式确保了迭代器是弱一致性(weakly consistent)的。迭代器的 next()
方法总是读取快照中的元素,因此不会抛出 ConcurrentModificationException
异常。
3. CopyOnWriteArrayList
的应用场景
CopyOnWriteArrayList
非常适合以下场景:
-
读操作远多于写操作:如果一个集合的读操作非常频繁,而写操作相对较少,那么
CopyOnWriteArrayList
是一个很好的选择。 -
迭代过程中不希望受到修改的影响:如果需要在迭代过程中避免
ConcurrentModificationException
异常的出现,CopyOnWriteArrayList
是一个很好的选择,因为它的迭代器是基于快照的。 -
需要线程安全的 List:如果需要线程安全的
List
实现,并且写操作不会特别频繁,可以选择CopyOnWriteArrayList
。
示例应用场景
-
缓存数据的场景:假设一个应用程序需要缓存一些数据,这些数据需要频繁读取,但偶尔会更新。使用
CopyOnWriteArrayList
可以在不加锁的情况下高效读取缓存数据,同时支持缓存的安全更新。 -
发布订阅模式:在实现发布订阅模式时,订阅者列表通常会被频繁遍历(读取),而订阅者的添加和移除相对较少,
CopyOnWriteArrayList
是一个理想的选择。
4. CopyOnWriteArrayList
的优缺点
4.1 优点
- 线程安全:由于读操作无锁,写操作采用独立副本,
CopyOnWriteArrayList
是线程安全的。 - 高效的读操作:在多线程环境下,读操作不需要加锁,非常高效。
- 避免了
ConcurrentModificationException
:由于迭代器是基于底层数组的快照,迭代过程中不会出现并发修改异常。
4.2 缺点
- 内存开销大:每次写操作都会创建数组副本,如果列表很大,频繁写操作会导致大量的内存消耗。
- 不适合写多读少的场景:由于写操作需要复制数组,写操作的开销较大,在写操作频繁的场景下性能较差。
5. 与其他线程安全集合的对比
-
与
Vector
相比:Vector
通过同步方法保证线程安全,但每个操作都需要获取锁,会导致较高的锁争用。而CopyOnWriteArrayList
在读操作上不加锁,因此在读多写少的场景下性能更优。 -
与
Collections.synchronizedList()
相比:Collections.synchronizedList()
也是通过同步方法实现线程安全,锁粒度大,同步开销高,CopyOnWriteArrayList
在高并发读操作下表现更好。
6. 总结
CopyOnWriteArrayList
是一种适用于读多写少场景的线程安全集合,通过写时复制机制提供了高效的读性能和线程安全性。它的设计理念和实现方式使其非常适合在需要高效读操作的多线程环境中使用。然而,由于写操作的内存开销和性能问题,在选择使用 CopyOnWriteArrayList
时,应根据具体的使用场景权衡其优缺点。