一文带你吃透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. 避坑指南

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

七、源码关键点

  1. 数组替换

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

    • 所有修改操作(add、set、remove)均通过 ReentrantLock 同步。

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

更多分享

  1. 一文带你吃透Android中常见的高效数据结构
  2. 详解:ArrayMap和SparseArray在HashMap上面的改进
  3. 详解:HashMap与TreeMap、HashTable的区别
  4. 详解:Set集合是如何保证元素不重复的
  5. 详解:LinkedHashMap的工作原理和实现
  6. 一文带你搞懂HashSet和TreeSet的区别
相关推荐
码农小河662 小时前
基于SSM+JSP的高校专业信息管理系统【附源码+数据库】
java·开发语言·数据库
张小姐的猫2 小时前
【MySQL】访问 & 数据流动原理
android·数据库·mysql·adb·1024程序员节
风若飞4 小时前
Spring Ai 入门遇到问题1
java·后端·spring
Nuanyt5 小时前
LeetCode Hot 100 链表专题(五)· 2 题(Java)
java·算法·leetcode·链表
朱涛的自习室5 小时前
212 个需求、22 万行代码:Agent 接管开源项目的 19 天
android·前端·人工智能
黑马程序员毕设5 小时前
基于 Java 的餐饮点餐系统设计与应用报告
java·开发语言
SL_staff5 小时前
从文档工具到知识中枢:基于JVS数字底座构建企业级统一内容管理平台的技术实践
java·开源·全栈
SL_staff5 小时前
从‘连得上’到‘管得好’:JVS-IOT双引擎驱动设备运维闭环
java·物联网·产品
w_zero_one8 小时前
递归算法(一)
java·开发语言·算法
SL_staff8 小时前
私有化低代码不是减配版开发:JVS双模式建模如何重建IT交付信任
java·低代码·spring cloud