CopyOnWriteArrayList 源码分析

CopyOnWriteArrayList 源码分析

CopyOnWriteArrayList 是 Java 并发包(java.util.concurrent)提供的线程安全 List 实现,核心设计思想是 "读写分离 + 写时复制" ,既能保证线程安全,又能避免读写互斥导致的性能损耗,适用于 读多写少 的并发场景。

一、核心特性概述

  1. 线程安全:读操作无锁,写操作加独占锁,避免并发修改问题;
  2. 写时复制(Copy-On-Write):写操作(add/remove/set)不会直接修改原数组,而是复制一份新数组进行修改,修改完成后替换原数组引用;
  3. 弱一致性 :读操作(get / 迭代)基于原数组快照,可能读取到 "旧数据",但不会抛出 ConcurrentModificationException
  4. 无快速失败(fail-fast):迭代器遍历期间集合被修改,不会触发快速失败机制。

二、类结构与核心成员

2.1 类继承与实现

java 复制代码
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // ...
}
  • 实现 List 接口:具备 List 集合的基本特性(有序、可重复);
  • 实现 RandomAccess 接口:支持随机访问(通过索引快速获取元素);
  • 实现 Cloneable/Serializable 接口:支持克隆和序列化。

2.2 核心成员变量

java 复制代码
// 存储元素的核心数组(volatile 保证读操作可见性)
private transient volatile Object[] array;

// 写操作的独占锁(保证写操作互斥)
private final transient ReentrantLock lock = new ReentrantLock();
  • array :用 volatile 修饰,确保当数组被替换时,所有读线程能立即看到最新的数组引用;
  • lockReentrantLock 独占锁,保证同一时间只有一个写操作执行(避免多线程同时复制数组导致数据不一致)。

三、构造方法

CopyOnWriteArrayList 提供 3 个核心构造方法(JDK 8),核心是初始化 array 数组:

3.1 无参构造(默认初始化)

java 复制代码
public CopyOnWriteArrayList() {
    // 初始化空数组(不可变空数组常量)
    setArray(new Object[0]);
}

// 封装 array 的赋值操作(私有方法)
private void setArray(Object[] a) {
    array = a;
}

3.2 基于集合初始化

java 复制代码
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    // 优化:如果传入的是 CopyOnWriteArrayList,直接复用其 array
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        // 否则转为数组(toArray() 可能返回非 Object[] 类型,需拷贝)
        elements = c.toArray();
        // c.toArray() 可能返回的是 c 自身的内部数组,需复制一份避免外部修改
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

3.3 基于数组初始化(包访问权限)

java 复制代码
// 仅用于内部或同包调用,直接使用传入的数组(不复制,需确保数组不可变)
CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

四、核心方法解析

4.1 读操作(无锁,高效)

读操作(get/contains/indexOf)无需加锁,直接基于当前 array 快照访问,性能极高。

4.1.1 get 方法(按索引获取元素)
java 复制代码
public E get(int index) {
    // 1. 获取当前 array 快照(volatile 保证可见性)
    // 2. 直接通过索引访问(RandomAccess 特性)
    return get(getArray(), index);
}

// 封装 array 的获取操作(私有方法)
private Object[] getArray() {
    return array;
}

// 实际获取元素(无锁,直接访问数组)
private E get(Object[] a, int index) {
    return (E) a[index];
}
  • 关键 :读操作不修改数组,且 arrayvolatile,所以无需加锁;
  • 弱一致性 :如果读操作执行时,写操作正在复制新数组但未替换 array,读线程仍会读取旧数组的元素。
4.1.2 contains 方法(判断元素是否存在)
java 复制代码
public boolean contains(Object o) {
    // 获取当前 array 快照,遍历判断
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length) >= 0;
}

// 私有工具方法:遍历数组查找元素(支持 null 元素)
private static int indexOf(Object o, Object[] elements, int index, int fence) {
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null)
                return i;
    } else {
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    return -1;
}
  • 支持 null 元素(通过 == 判断);
  • 基于快照遍历,不影响写操作。

4.2 写操作(加锁,复制数组)

所有写操作(add/remove/set)都需通过 lock 加锁,确保互斥,核心流程:

  1. 加锁;
  2. 获取当前数组快照;
  3. 复制一份新数组(修改长度或元素);
  4. 在新数组上执行写操作;
  5. 替换 array 引用为新数组;
  6. 解锁。
4.2.1 add 方法(末尾添加元素)
java 复制代码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock(); // 加独占锁,保证写操作互斥
    try {
        Object[] elements = getArray(); // 获取当前数组
        int len = elements.length;
        // 复制新数组(长度 +1)
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e; // 新元素添加到末尾
        setArray(newElements); // 替换原数组引用
        return true;
    } finally {
        lock.unlock(); // 最终解锁(避免异常导致死锁)
    }
}
4.2.2 add (int index, E element) 方法(指定索引添加)
java 复制代码
public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 检查索引合法性(index 不能大于 len)
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0) {
            // 索引 == 长度(末尾添加),直接复制 len+1 长度数组
            newElements = Arrays.copyOf(elements, len + 1);
        } else {
            // 索引在中间:创建 len+1 长度新数组,分两次复制
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index); // 复制前半部分
            System.arraycopy(elements, index, newElements, index + 1, numMoved); // 复制后半部分
        }
        newElements[index] = element; // 插入新元素
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}
4.2.3 remove 方法(删除指定索引元素)
java 复制代码
public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index); // 先获取要删除的旧元素
        int numMoved = len - index - 1;
        if (numMoved == 0) {
            // 删除最后一个元素,复制 len-1 长度数组
            setArray(Arrays.copyOf(elements, len - 1));
        } else {
            // 中间元素删除:创建 len-1 长度新数组,分两次复制(跳过删除元素)
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index, numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}
4.2.4 set 方法(修改指定索引元素)
java 复制代码
public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index); // 获取旧值
        if (oldValue != element) { // 元素不同才修改(优化)
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len); // 复制新数组
            newElements[index] = element; // 修改新数组元素
            setArray(newElements); // 替换原数组
        } else {
            // 元素相同,无需修改(避免复制数组,提升性能)
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}
  • 优化点:如果新元素与旧元素相同,不复制数组,直接复用原数组。

4.3 迭代器(弱一致性)

CopyOnWriteArrayList 的迭代器 COWIterator弱一致性 的,核心特性:

  • 迭代器创建时,持有当前 array 的快照(snapshot);
  • 迭代期间集合被修改(替换 array),迭代器仍遍历快照,不会抛出 ConcurrentModificationException
  • 迭代器不支持 remove 操作(会抛 UnsupportedOperationException)。
迭代器核心源码
java 复制代码
private static class COWIterator<E> implements ListIterator<E> {
    // 迭代器创建时的数组快照(不可变)
    private final Object[] snapshot;
    // 当前迭代位置
    private int cursor;

    // 构造器:传入创建时的 array 快照
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    // 是否有下一个元素(基于快照判断)
    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    // 获取下一个元素(基于快照)
    @SuppressWarnings("unchecked")
    public E next() {
        if (!hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    // 不支持 remove 操作
    public void remove() {
        throw new UnsupportedOperationException();
    }

    // ... 其他方法(hasPrevious、previous 等,均基于快照)
}
  • 弱一致性的原因:快照与原数组解耦,写操作修改的是新数组,不影响快照。

五、并发安全性分析

5.1 读操作安全

  • 读操作无锁,依赖 arrayvolatile 特性:确保读线程能立即看到最新的数组引用;
  • 数组本身是 "不可变" 的(写操作通过复制新数组修改),读线程遍历数组时不会出现 "数组长度变化" 或 "元素被中途修改" 的问题。

5.2 写操作安全

  • 写操作通过 ReentrantLock 独占锁保证互斥:避免多个写线程同时复制数组,导致数据覆盖;
  • 写操作修改的是新数组,修改完成后才替换 array 引用,不会影响正在进行的读操作。

5.3 弱一致性的本质

  • 读操作可能读取到 "旧数据":因为写操作的数组复制和替换需要时间,读线程可能在替换前获取到旧数组;
  • 适用于 "最终一致性" 场景,不适用于强一致性要求(如金融交易)。

六、优缺点总结

6.1 优点

  1. 读性能极高:读操作无锁,支持高并发读;
  2. 线程安全:无需手动加锁,避免并发修改问题;
  3. 迭代稳定 :迭代器不会抛出 ConcurrentModificationException,遍历过程平滑。

6.2 缺点

  1. 写性能差:每次写操作都要复制数组,时间复杂度 O (n),且消耗额外内存(最坏情况下双倍内存);
  2. 弱一致性:读操作可能获取旧数据,不适合强一致性场景;
  3. 内存占用高:写操作期间,原数组和新数组同时存在,大数据量场景下内存压力大。

七、适用场景与对比

7.1 适用场景

  • 读多写少:如配置中心、缓存列表、静态数据查询(读操作占比 90%+,写操作极少);
  • 最终一致性需求:允许读操作短暂获取旧数据,无需强实时性。

7.2 与其他 List 对比

特性 CopyOnWriteArrayList ArrayList Vector
线程安全 是(读写分离) 是(synchronized)
读性能 极高(无锁) 高(非并发) 低(读写加锁)
写性能 低(复制数组) 高(非并发) 低(同步锁)
迭代器特性 弱一致性(不抛异常) 快速失败(抛异常) 快速失败(抛异常)
内存占用 高(写时复制)

八、注意事项

  1. 元素特性 :支持 null 元素,依赖 equals 方法实现 contains/indexOf 等操作;
  2. 批量操作优化 :使用 addAll/removeAll 等批量方法,避免多次单个写操作(减少数组复制次数);
  3. 避免写频繁场景:如高频添加 / 删除的场景(如秒杀库存列表),会导致严重的内存开销和性能下降;
  4. 序列化arraytransient 修饰,通过自定义序列化(writeObject/readObject)保证序列化安全,避免序列化期间数组被修改。

总结

CopyOnWriteArrayList 是 "读多写少" 并发场景的最优解之一,其核心设计 "写时复制" 巧妙平衡了线程安全和读性能。但需注意其弱一致性和写操作的内存 / 性能开销,避免在写频繁或强一致性场景中使用。理解其源码设计(volatile 数组 + 独占锁 + 快照迭代器),能帮助我们更合理地选择并发集合。

相关推荐
廋到被风吹走1 小时前
【Spring】两大核心基石 IoC和 AOP
java·spring
晚风(●•σ )1 小时前
C++语言程序设计——【算法竞赛常用知识点】
开发语言·c++·算法
明有所思1 小时前
springsecurity更换加密方式
java·spring
Byron Loong1 小时前
【C#】离线场景检测系统时间回拨
开发语言·c#
浅川.251 小时前
xtuoj 哈希
算法·哈希算法·散列表
却话巴山夜雨时i1 小时前
295. 数据流的中位数【困难】
java·服务器·前端
free-elcmacom1 小时前
机器学习入门<4>RBFN算法详解
开发语言·人工智能·python·算法·机器学习
韭菜钟1 小时前
在Qt中实现mqtt客户端
开发语言·qt