opyOnWriteArrayList
是一种线程安全的 List 实现,通过“写时复制”来保证线程安全性。它在读取时提供的是一致性的快照数据,而不是实时更新的数据。在写入时则会创建一个新的数组,因此,读取操作不会看到正在写入或扩容中的中间状态。
1. CopyOnWriteArrayList
的核心数据结构
- 底层数据结构:
CopyOnWriteArrayList
是基于动态数组的实现,其底层维护了一个Object[]
数组。 - 写时复制:在写操作(如
add()
、set()
、remove()
等)时,会创建一个新数组,在新数组上进行操作,然后将新数组赋值给底层的数组引用。
2. add()
方法的实现逻辑
2.1 添加元素的步骤
- 步骤 1:复制当前数组:
- 在添加元素时,
CopyOnWriteArrayList
会首先复制当前底层数组到一个新数组中。
- 在添加元素时,
- 步骤 2:在新数组上添加元素:
- 在新数组的最后一个位置添加新元素。
- 步骤 3:将新数组替换旧数组:
- 使用原子性操作,将新的数组替换旧的底层数组,使其指向新的内存空间。
代码示例
javaCopy codepublic boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1. 获取锁
try {
Object[] elements = getArray(); // 2. 获取当前数组
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 3. 复制并扩容
newElements[len] = e; // 4. 添加新元素
setArray(newElements); // 5. 替换为新数组
return true;
} finally {
lock.unlock(); // 6. 释放锁
}
}
原理说明
- 获取锁:在执行写操作时,会通过
ReentrantLock
加锁,确保在添加新元素时不会有其他线程进行写入。 - 复制并扩容:
CopyOnWriteArrayList
的写操作会复制整个数组,因此每次写操作的开销是线性 O(n) 的。 - 最终一致性:由于写时创建了一个新数组,因此在写操作完成前,其他线程的读取操作不会受到影响。
3. get()
方法的实现逻辑
3.1 读取元素的步骤
- 步骤 1:读取数组引用:
- 在
get()
操作中,CopyOnWriteArrayList
会直接读取底层数组引用,获取当前快照数据。
- 在
- 步骤 2:返回元素:
- 根据索引直接从数组中返回元素。
代码示例
javaCopy codepublic E get(int index) {
Object[] elements = getArray(); // 1. 获取当前数组
return (E) elements[index]; // 2. 返回指定索引的元素
}
原理说明
- 无锁读取:
get()
方法是无锁的,直接读取底层数组,因此性能非常高。 - 读取快照:由于读取的数组是写时复制产生的快照,即使写操作正在进行,读取操作也不会被阻塞,且能获取到一致的旧数据。
4. 扩容的实现逻辑
4.1 扩容时的处理
- 扩容在写操作时发生,例如在
add()
、set()
或remove()
操作中。CopyOnWriteArrayList
不会像ArrayList
那样通过自动扩容实现容量增长,而是通过每次复制整个数组来扩容。
扩容和写入的详细步骤
- 锁定操作:写入和扩容需要获取锁,通过
ReentrantLock
实现独占锁定。 - 复制和扩容:写入或扩容操作时,会创建一个比当前数组更大的新数组,将现有元素复制到新数组中。
- 插入新元素:在新数组中执行插入或更新操作。
- 替换旧数组:将新数组设置为底层数组。
5. 在读取时的影响
读取操作是否会受到扩容影响?
- 不会受到扩容影响:由于
CopyOnWriteArrayList
在写入时总是复制整个数组,且复制完成后才会将新数组替换为当前数组,读取操作始终访问的是旧数组快照数据,因此不会读取到扩容过程中的中间状态。 - 可能读到旧数据:在写操作进行时,读取操作仍然会获取到写操作前的旧数据,写操作完成后,新的读取操作才能看到最新数据。
6. 多线程下的写时复制机制
CopyOnWriteArrayList
通过“写时复制”来确保线程安全。当多个线程同时进行写操作时,由于写操作需要获取锁,因此写入是互斥的。- 读取操作是无锁的,读取线程只会读取到旧的快照数据,这种设计确保了读取的高效性和一致性。
7. 性能分析和适用场景
性能分析
- 读操作效率高:
get()
方法是无锁的,直接访问底层数组,因此读操作效率非常高。 - 写操作性能较低:由于每次写操作都需要复制整个数组,因此写操作的时间复杂度是 O(n),在写操作频繁时性能较低。
- 内存消耗高:由于每次写操作都会创建一个新数组,因此在写操作频繁时,会消耗较多的内存。
适用场景
- 适用于读多写少的场景,如事件监听器列表、配置快照、缓存快照等。
- 不适用于写操作频繁的场景,因为大量的写操作会导致性能瓶颈和内存消耗。
8. 总结
CopyOnWriteArrayList
在扩容或写入时,通过写时复制机制保证了线程安全,但读取操作只能获取到旧的快照数据。- 读取操作不会被扩容和写入操作阻塞,但无法读取到最新的数据,直到写操作完成。
- 适合读多写少的场景,在写操作频繁的情况下可能引发性能瓶颈。