中 进阶
CopyOnWriteArrayList原理#
一句话答案#
写时复制:读无锁直接读数组,写加锁复制新数组修改后替换原数组,适合读多写少场景(如监听器列表)。
核心要点
核心原理:
- 读:无锁,直接读取内部
Object[] array(volatile 保证可见性) - 写:加 ReentrantLock → 复制底层数组 → 修改副本 → 替换原数组 → 释放锁
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] old = getArray();
Object[] newArr = Arrays.copyOf(old, old.length + 1);
newArr[old.length] = e;
setArray(newArr); // volatile 写
return true;
} finally { lock.unlock(); }
}java优缺点:
- ✅ 读无锁,并发读性能极高
- ✅ 迭代器不会抛 ConcurrentModificationException
- ❌ 写操作慢(复制整个数组)
- ❌ 内存占用(写时两份数组)
- ❌ 数据弱一致性(读到旧数据)
适用场景: 事件监听器列表、配置项缓存(读远多于写)
面试回答(2分钟版)
CopyOnWriteArrayList 是 JUC 提供的线程安全 List,核心思想是写时复制:读操作直接读取内部的 volatile 数组引用,完全无锁无开销;写操作先加 ReentrantLock,然后复制一份新数组,在新数组上做修改,最后通过 volatile 写替换原数组引用,释放锁。这样读线程始终读的是修改前的快照,不会受写操作影响,也不会抛出 ConcurrentModificationException。代价是写操作需要复制整个数组,时间复杂度 O(n),而且写入期间内存中同时存在新旧两份数组。另一个特点是弱一致性:读线程可能读到旧数据,因为读的是写入前的数组快照。所以 CopyOnWriteArrayList 只适合读多写极少的场景,比如事件监听器列表、配置项缓存、黑白名单等。如果写操作频繁应该用 Collections.synchronizedList 或者 ConcurrentLinkedQueue 等其他并发容器。
追问与易错
追问方向:
- “写时复制的内存开销大吗?”→ 每次写操作复制整个数组,写入期间新旧两份数组同时存在,数组越大内存开销越大,所以只适合小数据量且写操作极少的场景
- “迭代器是强一致还是弱一致?”→ 弱一致性,迭代器持有的是创建时的数组快照,遍历期间其他线程的写入不可见,不会抛 ConcurrentModificationException
- “什么场景适合用?”→ 读多写极少的场景,如事件监听器列表、配置项缓存、黑白名单;写频繁应使用 Collections.synchronizedList 或 ConcurrentLinkedQueue
易错点:
- ❌ 写也是 O(1)——每次写复制整个数组 O(n)
- ❌ 在写多场景使用——性能极差