一文带你吃透CopyWriteArrayList的内部实现

CopyOnWriteArrayList 是 Java 并发包中提供的线程安全列表,采用 写时复制(Copy-On-Write) 策略实现高并发读取,适用于 读多写少 的场景。本文将从数据结构、核心操作、优缺点等方面进行详细解析其实现机制。

一、数据结构

  1. 底层存储

    • 使用 volatile 修饰的数组保存元素,确保多线程可见性:

      java 复制代码
      private transient volatile Object[] array;
    • 所有操作均基于此数组的快照,写操作会复制并替换原数组。

  2. 锁机制

    • 通过 ReentrantLock 保证写操作的原子性:

      java 复制代码
      final transient ReentrantLock lock = new ReentrantLock();

二、核心操作

1. 写操作(Add/Set/Remove)

  • 步骤

    1. 加锁 :获取 ReentrantLock
    2. 复制数组:创建原数组的副本。
    3. 修改副本:在副本上执行写操作(如添加、删除元素)。
    4. 替换原数组 :将 volatile 数组引用指向新副本。
    5. 释放锁:解锁,允许其他写操作进行。
  • 示例(add 方法)

    java 复制代码
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements); // volatile 写,保证可见性
            return true;
        } finally {
            lock.unlock();
        }
    }

2. 读操作(Get/Iterate)

  • 无锁访问:直接读取当前数组,无需同步。

  • 示例(get 方法)

    java 复制代码
    public E get(int index) {
        return get(getArray(), index); // 直接访问 volatile 数组
    }

3. 迭代器

  • 弱一致性:迭代器基于创建时的数组快照,不会反映后续修改。

  • 实现代码

    java 复制代码
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    • COWIterator 内部保存快照数组,遍历期间不受写操作影响。

三、性能与适用场景

场景 性能表现 示例应用
高频读取 极高(无锁,直接访问数组) 事件监听器列表、配置信息缓存
低频写入 较低(复制数组开销大) 动态添加少量监听器或配置项
迭代遍历 安全但滞后(基于旧快照) 日志记录、批量读取操作

四、优缺点分析

优点

  • 线程安全:写操作通过锁和复制保证原子性,读操作无锁。
  • 无并发修改异常 :迭代器遍历旧快照,不会抛出 ConcurrentModificationException
  • 高读性能:读操作无锁,适合读多写少场景。

缺点

  • 写性能差 :每次写操作需复制整个数组,时间复杂度为 O(n)
  • 内存占用高:频繁写操作导致内存碎片和临时对象增加。
  • 数据延迟:迭代器和读操作可能访问旧数据,无法保证强一致性。

五、对比其他线程安全列表

实现类 锁机制 读性能 写性能 一致性
CopyOnWriteArrayList 写时复制 + 锁 极高 弱一致性(快照)
Vector 全表锁 中等 强一致性
Collections.synchronizedList 全表锁 中等 强一致性

六、使用建议

  1. 适用场景

    • 监听器管理(如 GUI 事件监听)。
    • 读多写少的配置或缓存数据。
    • 需要避免迭代时并发修改异常的场合。
  2. 避坑指南

    • 避免频繁写操作 :如实时数据更新,应选择 ConcurrentLinkedQueueConcurrentHashMap
    • 监控内存使用:大数组频繁复制可能导致 GC 压力。
    • 慎用批量写入:尽量合并多次写操作,减少复制次数。

七、源码关键点

  1. 数组替换

    java 复制代码
    final void setArray(Object[] a) {
        array = a; // volatile 写,保证线程可见性
    }
  2. 锁的使用

    • 所有修改操作(addsetremove)均通过 ReentrantLock 同步。

通过写时复制策略,CopyOnWriteArrayList 在特定场景下实现了高效的线程安全读取,是 Java 并发编程中处理读多写少问题的经典设计。开发者需根据实际需求权衡其优缺点,合理选择数据结构。

更多分享

  1. 一文带你吃透Android中常见的高效数据结构
  2. 详解:ArrayMap和SparseArray在HashMap上面的改进
  3. 详解:HashMap与TreeMap、HashTable的区别
  4. 详解:Set集合是如何保证元素不重复的
  5. 详解:LinkedHashMap的工作原理和实现
  6. 一文带你搞懂HashSet和TreeSet的区别
相关推荐
dddaidai1231 分钟前
Redis数据结构
数据结构·redis·hash table
武昌库里写JAVA10 分钟前
MacOS Python3安装
java·开发语言·spring boot·学习·课程设计
eternal__day17 分钟前
Spring Cloud:构建云原生微服务架构的最佳工具和实践
java·spring cloud·微服务·云原生·架构
cdut_suye17 分钟前
【Linux系统】从 C 语言文件操作到系统调用的核心原理
java·linux·数据结构·c++·人工智能·机器学习·云计算
dancing99918 分钟前
Android Studio中Gradle 7.0上下项目配置及镜像修改
android·ide·android studio
forestsea24 分钟前
Maven 插件参数注入与Mojo开发详解
java·maven·mojo
荔枝吻1 小时前
【抽丝剥茧知识讲解】引入mybtis-plus后,mapper实现方式
java·sql·mybatis
在未来等你1 小时前
互联网大厂Java求职面试:构建高并发直播平台的架构设计与优化
java·spring boot·微服务·kubernetes·高并发·分布式系统·直播平台
灵典3361 小时前
数据结构入门-二叉树的层序遍历
数据结构·算法
轮到我狗叫了1 小时前
力扣.1471数组的k个最强值,力扣.1471数组的k个最强值力扣1576.替换所有的问号力扣1419.数青蛙编辑力扣300.最长递增子序列
java·数据结构·算法