CopyOnWriteArrayList快照读能避开锁竞争,因其读操作直接访问volatile修饰的旧数组,不加锁;写操作则加锁复制新数组并原子替换引用,实现读写分离。

CopyOnWriteArrayList 的快照读为什么能避开锁竞争
因为每次写操作(add、remove、set)都会新建数组并原子替换引用,而读操作(get、iterator、contains)全程只读旧数组——读不阻塞写,写也不阻塞读,天然无锁。
这正好匹配白名单场景:读远多于写(如每秒千次校验 vs 每分钟一次更新),且允许读到“稍旧”的快照(新条目延迟最多一次写操作时间,通常可接受)。
白名单校验时怎么安全用 get() 和 contains() 而不触发迭代器问题
CopyOnWriteArrayList 的 get(int) 和 contains(Object) 都是直接查内部数组,不创建迭代器,也不复制数据,开销极低。但要注意:contains 是线性扫描,如果白名单很大(比如 >10k 条),建议配合 HashSet 做二级索引,否则可能拖慢校验路径。
常见误用是写成 list.stream().anyMatch(...)——这会触发 iterator(),虽仍无锁,但会额外拷贝当前快照的迭代器状态,纯属冗余。
- ✅ 推荐:
whitelist.contains(userId) - ❌ 避免:
whitelist.stream().filter(...).findAny().isPresent() - ⚠️ 注意:
whitelist.get(i)下标越界会抛IndexOutOfBoundsException,校验前别假设 size 不变(虽然读不到中间态,但 size 是写后立即更新的)
动态更新白名单时,如何避免 add/remove 引发的短暂内存抖动
每次写操作都要复制整个底层数组,如果白名单有 5 万条字符串,单次 add 可能分配数 MB 内存,并触发 Young GC。高频更新(比如秒级)会明显影响 STW。
实操上应限制更新节奏,并批量操作:
- 用
addAll(Collection)代替多次add,减少复制次数 - 更新前先比对新旧集合差异(如用
HashSet求差集),只提交净变更 - 若需实时强一致(如封禁立即生效),
CopyOnWriteArrayList不适合,该换ConcurrentHashMap+ 读写锁
示例:从配置中心拉取新列表后,不要直接 whitelist.clear(); whitelist.addAll(newList);——clear() 本身也是写操作,会复制一次空数组;应直接 whitelist = new CopyOnWriteArrayList(newList)(注意引用替换需保证线程可见,建议用 volatile 字段包装)。
为什么不能把 CopyOnWriteArrayList 当作通用线程安全 List 来用
它只在「读多写少 + 数据量小 + 允许最终一致性」时成立。白名单恰好满足,但换成订单列表或实时聊天消息就不行:
- 写吞吐低:100 并发
add可能导致数组反复复制,CPU 和内存带宽吃紧 - 内存占用高:旧数组在 GC 前一直存活,容易 OOM(尤其存大对象时)
- 迭代器看到的是构造时的快照,遍历时新增/删除不可见——这点常被忽略,比如用
for-each遍历同时后台在删元素,不会抛ConcurrentModificationException,但逻辑可能出错
真正关键的不是“能不能用”,而是“什么时候不该用”。白名单更新频率低、单次写入数据量可控、读操作绝不依赖实时性——这些约束条件漏掉任何一个,零锁优势就立刻反转成性能陷阱。










