CopyOnWriteArrayList适用于读多写少的线程安全场景,如配置列表;读操作无锁高效,写操作通过复制数组实现,但需注意非原子复合操作、迭代快照特性、内存开销及不支持null等限制。

在 Java 中,CopyOnWriteArrayList 是一种适合“读多写少”场景的线程安全列表,特别适用于配置项、监听器列表、规则列表等需要频繁读取但极少修改的动态配置场景。它通过写时复制(Copy-On-Write)机制实现读写分离:读操作无锁、不阻塞;写操作则复制底层数组、更新副本、再原子替换引用——天然避免了并发读写冲突。
为什么用 CopyOnWriteArrayList 管理配置列表?
配置列表通常具备以下特点:启动时加载一次,运行中只偶尔更新(如热刷新)、但被大量线程高频读取(如每次请求校验配置)。传统方案如 synchronized List 或 ReentrantLock 会严重拖慢读性能;而 ConcurrentHashMap 不适合有序列表场景;Vector 或 Collections.synchronizedList 读写都加锁,吞吐低。CopyOnWriteArrayList 正好匹配:读不加锁、迭代安全、写操作虽有复制开销但发生频率低,整体性价比高。
典型使用方式:初始化 + 安全更新 + 无锁读取
以一个动态黑白名单配置为例:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
-
初始化:从配置中心或本地文件加载初始数据,构造为
CopyOnWriteArrayList -
读取(任意线程):直接调用
get()、contains()、或增强 for 循环遍历——全程无锁,不会抛ConcurrentModificationException -
更新(由配置监听器/管理接口触发):调用
set()、add()、remove()等方法——内部自动复制数组并替换引用,对正在读取的线程完全透明
关键注意事项与避坑指南
尽管易用,但几个细节直接影响正确性与性能:
-
写操作不是原子复合操作:比如
list.removeIf(x -> x.isExpired())是线程安全的,但if (list.contains(x)) list.remove(x)不是原子的,可能因中间插入导致误判——应改用单次写方法或加外部同步 - 迭代期间看不到本次写入:因为迭代基于快照,新元素要等下次迭代才可见。若业务依赖“写后立即可读”,需确认是否接受最终一致性
- 内存与 GC 开销不可忽视:每次写都复制整个数组,大数据量(如数万条配置)+ 频繁更新会导致内存暴涨和 GC 压力。建议仅用于几百至几千条以内、更新间隔大于秒级的配置
-
不支持 null 元素:构造或添加
null会抛NullPointerException,解析配置时需提前过滤
配合配置中心实现热更新的简明示例
假设使用 Nacos 监听配置变更:
立即学习“Java免费学习笔记(深入)”;
private final CopyOnWriteArrayList<String> blackList = new CopyOnWriteArrayList<>();
// 启动时加载
blackList.addAll(loadFromConfigServer());
// Nacos 回调中更新
nacosListener.onUpdate(configStr -> {
List<String> newList = parseBlackList(configStr);
blackList.clear(); // 注意:clear() 也是写操作,线程安全
blackList.addAll(newList); // 批量添加更高效,比逐个 add() 少一次复制
});
// 业务代码中读取(无锁、无异常)
public boolean isBlocked(String ip) {
return blackList.contains(ip); // O(n),适合小规模;若需高性能查找,考虑额外建 Set 缓存
}

















