配置监听器和事件订阅列表读多写少,你会考虑 CopyOnWriteArrayList 吗?它的边界是什么?
先说结论:CopyOnWriteArrayList 写操作加锁并复制整个底层数组,修改完成后发布新数组;读操作无需加锁,迭代器读取创建时的稳定快照。它适合监听器、配置列表等读远多于写且规模有限的场景。
我先给结论,再说明它在项目里解决什么问题。理解写时复制、快照迭代及其在读多写少场景下的收益与代价。CopyOnWriteArrayList 写操作加锁并复制整个底层数组,修改完成后发布新数组;读操作无需加锁,迭代器读取创建时的稳定快照。它适合监听器、配置列表等读远多于写且规模有限的场景。 写入成本是 O(n),高频写或大集合会造成复制、内存峰值和 GC 压力,因此不能把它当作通用线程安全 List。
核心机制我会按一次真实执行过程来讲。沿着「读取当前数组快照 → 写操作获取互斥锁 → 复制完整数组 → 在副本上修改 → 发布新数组引用」观察输入、状态与输出,这些阶段都可以从日志、指标或源码里验证。 读取当前数组快照 读取只抓取当前 volatile 数组引用,因此无需锁,迭代器也基于创建时的固定快照。 写操作获取互斥锁 所有写入串行获取锁,避免多个写线程互相覆盖副本。 复制完整数组 每次写都复制整个数组,时间和临时内存与元素数量成正比,大列表写入成本很高。
实现细节只抓关键入口,不会整段背源码。CopyOnWriteArrayList#add:加锁、Arrays.copyOf、setArray。 COWIterator:构造时固定 snapshot 引用。 我会先确认请求实际走到了哪条路径,再用运行数据验证,不会只看类名或配置猜测。
放到生产使用时,我会关注参数和验证数据。列表规模从 10 到 100k,分别逐个 add 与单次 addAll;JFR 记录复制数组字节和旧快照晋升。 固定输入和基线 先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「单次写复制字节」为主基线,记录值应满足「约等于数组引用大小」;同时保存 列表长度、单位时间写次数,使后续变化能够回到同一时间轴比较。
最后补充常见误区和使用边界。监听器注册本来很少,但一次配置重载逐个 add 数千项,每次都复制不断增长的数组,产生大量短命对象。改为先构建普通 List,再一次 addAll 发布,并限制列表规模后,保留了无锁读取优势。 大列表频繁单元素写导致复制风暴:列表长度:逐项更新改为构建后一次发布。 误以为迭代器能看到最新元素:单位时间写次数:限制监听器列表规模。 方案:更适合的场景:主要收益:代价与边界。 CopyOnWriteArrayList:小规模、读极多写极少且允许快照读取:读无锁、遍历稳定:写放大和旧快照占内存。 同步 List:读写都不频繁且需简单互斥:语义直接:遍历需持锁,全局竞争。 不可变 List + volatile:整批替换配置快照:读取最轻、版本清晰:不适合高频单元素修改。