一文带你吃透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的区别
相关推荐
whycthe3 分钟前
c++竞赛常用函数
java·开发语言
Violet_YSWY18 分钟前
final是干嘛的
java·开发语言
RickyWasYoung19 分钟前
【matlab】字符串数组 转 double
android·java·javascript
lang2015092835 分钟前
Spring OXM:轻松实现Java-XML互转
xml·java·spring
南方的狮子先生1 小时前
【数据结构】(C++数据结构)查找算法与排序算法详解
数据结构·c++·学习·算法·排序算法·1024程序员节
Lisonseekpan1 小时前
为什么要避免使用 `SELECT *`?
java·数据库·后端·sql·mysql·oracle
一只小透明啊啊啊啊1 小时前
Java的中间件
java·开发语言·中间件
Wilson Chen1 小时前
深入理解 MySQL 事务与锁机制:从 ACID 到 Next-Key Lock 的实证之旅
java·数据库·mysql
bluetata1 小时前
Rokid AR眼镜开发入门:构建智能演讲提词器Android应用
android·人工智能·云计算·ar·ai编程
学编程就要猛1 小时前
数据结构初阶:Java中的ArrayList
java·开发语言·数据结构