CopyOnWriteArrayList
CopyOnWriteArrayList 是线程安全的 List,核心思想是写时复制。读操作不加锁,写操作复制底层数组并替换引用。
# 1. 写时复制模型
读线程
│
▼
读取当前数组快照
写线程
│
▼
加锁 -> 复制数组 -> 修改新数组 -> 替换数组引用
读写互不阻塞,读操作非常快,写操作成本高。
# 2. 示例
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
for (String value : list) {
System.out.println(value);
}
适合读多写少的数据。
# 3. 迭代快照
迭代器基于创建时的数组快照。
Iterator<String> iterator = list.iterator();
list.add("C");
while (iterator.hasNext()) {
System.out.println(iterator.next()); // 看不到 C
}
模型:
iterator -> old array [A, B]
list -> new array [A, B, C]
因此遍历不会抛 ConcurrentModificationException,但也看不到之后的修改。
# 4. 适用场景
适合:
- 监听器列表。
- 配置快照。
- 黑白名单。
- 读远多于写。
- 列表规模不大。
不适合:
- 高频写入。
- 大列表。
- 强实时一致读取。
# 5. addIfAbsent
list.addIfAbsent("A");
如果元素不存在才添加。适合监听器注册等场景。
但它仍要遍历判断和写时复制,数据量大时成本不低。
# 6. 与 synchronizedList 对比
| 对比项 | CopyOnWriteArrayList | synchronizedList |
|---|---|---|
| 读操作 | 无锁快照读 | 加锁 |
| 写操作 | 复制数组,成本高 | 加锁修改 |
| 迭代 | 快照,不 fail-fast | 遍历需手动同步 |
| 适合 | 读多写少 | 读写都不太高的简单同步 |
# 7. 内存成本
写入时会同时存在旧数组和新数组:
old array [A, B, C]
new array [A, B, C, D]
大列表频繁写入会带来内存和 GC 压力。
# 专家实践与边界
CopyOnWriteArrayList 的核心思想是读不加锁,写时复制新数组。它牺牲写入性能和内存,换取读路径的简单稳定。
读操作
└─ 直接读取当前数组快照
写操作
├─ 加锁
├─ 复制旧数组
├─ 修改新数组
└─ 替换引用
| 场景 | 是否适合 | 原因 |
|---|---|---|
| 配置监听器列表 | 适合 | 读多写少 |
| 黑白名单小集合 | 适合 | 迭代频繁,修改少 |
| 高频写入列表 | 不适合 | 每次写复制数组 |
| 大集合 | 不适合 | 内存复制成本高 |
| 强实时一致遍历 | 谨慎 | 迭代器是快照 |
它非常适合“读远多于写且集合不大”的场景。看到高频 add/remove,应该优先考虑其他并发结构或读写锁方案。
# Tips 快问快答
Q:CopyOnWriteArrayList 适合什么场景? A:读多写少、列表不大的场景。
Q:写操作为什么慢? A:每次写都复制底层数组。
Q:迭代时能看到之后新增元素吗? A:看不到,迭代器基于创建时快照。
Q:会抛 ConcurrentModificationException 吗? A:通常不会。
Q:它适合高频写入吗? A:不适合,写入和 GC 成本很高。
Q:读取需要加锁吗? A:不需要,读当前数组引用。
Q:元素对象本身线程安全吗? A:不一定。列表结构安全不代表元素内部状态安全。
Q:大列表适合用它吗? A:通常不适合,复制成本太高。
上次更新: 2026/06/25, 14:19:18