addifabsent不是原子操作,因其先检查后添加的两步间存在竞态窗口,无锁保护导致并发时可能重复插入;源码中遍历判断与数组复制替换分离,无法保证“存在则跳过、不存在则插入”的原子语义。

CopyOnWriteArrayList 的 addIfAbsent 方法本身不是原子的,它只是“看起来像原子”,实际由两步组成:先检查元素是否存在,再决定是否添加。由于中间存在竞态窗口,无法真正保证线程安全地插入不重复元素。
为什么 addIfAbsent 不是原子操作
源码层面,addIfAbsent 逻辑如下:
- 遍历整个数组判断元素是否已存在(O(n) 时间)
- 若不存在,则创建新数组、复制旧数据、追加新元素、替换引用
这两步之间没有锁或 CAS 保护 —— 比如线程 A 判断完“不存在”,正准备复制数组时,线程 B 已插入相同元素并完成替换,A 还会继续插入,导致重复。
真正线程安全的替代方案
要实现“存在则跳过、不存在则插入”的原子语义,需借助更合适的并发结构:
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- ConcurrentHashMap + keySet():用 keySet().add(),底层基于 CAS 和 synchronized 分段控制,add 操作本身就是原子的
- ConcurrentSkipListSet:有序、线程安全,add() 天然去重且原子
- 显式加锁 + CopyOnWriteArrayList(不推荐):用 ReentrantLock 包裹 contains() + add(),但违背了 CopyOnWriteArrayList 无锁设计初衷,性能差
什么时候可以用 addIfAbsent
它适用于读多写少 + 允许短暂重复 + 对最终一致性可接受的场景,例如:
- 监听器列表:多个线程可能重复注册同一监听器,但重复调用影响不大
- 缓存预热任务队列:少量重复任务不会破坏业务逻辑
注意:它不适用于需要强唯一性保障的场景,比如用户 ID 去重注册、订单号防重提交。
简单验证重复插入的示例
以下代码在高并发下大概率出现重复:
(伪代码示意)- 线程1:调用 list.addIfAbsent("x") → 查到无"x" → 开始复制数组
- 线程2:同一时刻也调用 addIfAbsent("x") → 同样查到无"x" → 也开始复制数组
- 两者先后完成,结果 list 中出现两个"x"
这种现象在 CopyOnWriteArrayList 文档中明确说明:“此方法并非原子操作”。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










