CopyOnWrite容器

CopyOnWrite容器:读写分离的并发容器设计与实现

CopyOnWrite(写时复制)容器是一种通过读写分离延迟更新策略实现的高效并发容器,核心思想是:读操作直接访问当前容器,写操作则复制一份新容器进行修改,完成后再替换旧容器。这种设计在 “读多写少” 的场景下能显著提升并发性能,避免读写冲突。本文以 CopyOnWriteArrayList 为例,深入解析其原理、实现及适用场景。

CopyOnWrite 核心思想

读写分离

  • 读操作:直接访问当前容器(无锁),无需阻塞;
  • 写操作:不直接修改原容器,而是复制一份新容器进行修改,修改完成后通过原子操作替换旧容器;
  • 最终一致性:写操作期间,读操作仍访问旧容器,避免 “脏读”;写操作完成后,所有新读操作会访问新容器,保证数据最终一致。

适用场景

  • 读多写少:如配置缓存、白名单列表等,读取频率远高于修改频率;
  • 允许数据短暂不一致:写操作完成前,读操作可能访问旧数据,适用于对实时性要求不高的场景;
  • 遍历操作频繁:避免迭代时的 ConcurrentModificationException(传统同步容器在迭代中修改会抛此异常)。

CopyOnWriteArrayList 源码解析

CopyOnWriteArrayList 是 List 接口的并发实现,底层通过数组存储数据,核心源码如下:

核心成员变量

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {  
    // 独占锁:保证写操作的线程安全(同一时间只有一个线程执行写操作)  
    final transient ReentrantLock lock = new ReentrantLock();  

    // 存储元素的数组(volatile 保证可见性,读操作能及时看到最新容器)  
    private transient volatile Object[] array;  

    // 获取当前数组(读操作直接访问)  
    final Object[] getArray() {  
        return array;  
    }  

    // 设置新数组(写操作完成后替换旧数组)  
    final void setArray(Object[] a) {  
        array = a;  
    }  

    // 初始化:空数组  
    public CopyOnWriteArrayList() {  
        setArray(new Object[0]);  
    }  
}
  • lock:写操作时加锁,确保同一时间只有一个线程复制和修改容器,避免多线程写操作导致的副本混乱;
  • arrayvolatile 修饰的数组,保证读操作能立即看到最新的容器替换(写操作完成后 setArray 的可见性)。

写操作:add (E e)

写操作(添加、修改、删除)的核心是复制旧数组→修改新数组→替换旧数组

public boolean add(E e) {  
    final ReentrantLock lock = this.lock;  
    // 1. 加锁:保证唯一线程执行写操作  
    lock.lock();  
    try {  
        // 2. 获取当前数组(旧容器)  
        Object[] elements = getArray();  
        int len = elements.length;  

        // 3. 复制旧数组到新数组(容量+1)  
        Object[] newElements = Arrays.copyOf(elements, len + 1);  

        // 4. 在新数组中添加元素  
        newElements[len] = e;  

        // 5. 替换旧数组(原子操作,volatile 保证可见性)  
        setArray(newElements);  
        return true;  
    } finally {  
        // 6. 释放锁  
        lock.unlock();  
    }  
}

流程解析

  • 加锁确保写操作独占,避免多线程同时复制数组导致的资源浪费和数据不一致;
  • 复制旧数组时,新数组与旧数组是完全独立的内存空间,读操作可继续安全访问旧数组;
  • setArray 是原子操作,替换瞬间所有新读操作会访问新数组,实现 “最终一致性”。

读操作:get (int index)

读操作无需加锁,直接访问当前数组,性能极高:

public E get(int index) {  
    // 直接访问当前数组(无锁)  
    return get(getArray(), index);  
}  

private E get(Object[] a, int index) {  
    return (E) a[index];  
}

特点

  • 读操作无锁,无需等待,适合高并发读取场景;
  • 可能读取到旧数据(若写操作正在进行),但不会读到 “部分修改” 的中间状态(因写操作在新数组中完成)。

遍历操作:iterator ()

CopyOnWriteArrayList 的迭代器基于快照实现,遍历的是迭代器创建时的数组副本,避免 ConcurrentModificationException

public Iterator<E> iterator() {  
    return new COWIterator<E>(getArray(), 0);  
}  

static final class COWIterator<E> implements ListIterator<E> {  
    // 迭代器创建时的数组快照(不会被后续写操作修改)  
    private final Object[] snapshot;  
    private int cursor;  

    private COWIterator(Object[] elements, int initialCursor) {  
        cursor = initialCursor;  
        snapshot = elements;  // 保存当前数组的引用  
    }  

    public E next() {  
        if (!hasNext())  
            throw new NoSuchElementException();  
        return (E) snapshot[cursor++];  // 访问快照数组  
    }  

    // 迭代器不支持修改操作(否则抛 UnsupportedOperationException)  
    public void remove() {  
        throw new UnsupportedOperationException();  
    }  
}

优势

  • 迭代期间,即使原容器被修改,迭代器仍访问快照数组,不会抛出 ConcurrentModificationException
  • 适合需要频繁遍历且修改较少的场景(如日志查询、数据统计)。

CopyOnWriteArraySet:基于 CopyOnWriteArrayList 的 Set 实现

CopyOnWriteArraySet 内部通过 CopyOnWriteArrayList 实现,核心是利用其 “添加前检查是否存在” 的特性保证元素唯一性:

public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements Serializable {  
    private final CopyOnWriteArrayList<E> al;  

    public CopyOnWriteArraySet() {  
        al = new CopyOnWriteArrayList<>();  
    }  

    // 添加元素前先检查是否存在,不存在则添加  
    public boolean add(E e) {  
        return al.addIfAbsent(e);  
    }  
}  

// CopyOnWriteArrayList 中的 addIfAbsent 实现  
public boolean addIfAbsent(E e) {  
    Object[] snapshot = getArray();  
    // 先检查元素是否已存在(基于快照)  
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :  
        addIfAbsent(e, snapshot);  
}

特点

  • CopyOnWriteArrayList 共享相同的并发特性(读无锁、写复制);
  • 适合 “读多写少” 且需要去重的场景(如用户标签集合)。

优缺点分析

优点

  1. 高并发读性能优异:读操作无锁,无需阻塞,适合读多写少场景;
  2. 迭代安全:迭代器基于快照,不会抛出 ConcurrentModificationException
  3. 最终一致性:写操作不影响读操作,避免 “脏读” 和部分修改的中间状态。

缺点

  1. 内存占用翻倍:写操作时复制整个数组,若数组较大,会占用双倍内存;
  2. 写操作性能差:复制数组耗时(O (n) 时间复杂度),不适合频繁修改的场景;
  3. 数据实时性低:写操作完成前,读操作访问旧数据,可能导致短暂的数据不一致。

与其他并发容器的对比

容器类型 核心机制 读性能 写性能 内存占用 适用场景
CopyOnWriteArrayList 写时复制,读写分离 极高 较差 读多写少,允许短暂不一致
Vector 全量加锁(synchronized) 读写频率均衡,需强一致性
ConcurrentHashMap 分段锁,CAS 键值对存储,高并发读写

最佳实践

  1. 场景选择:仅在 “读操作远多于写操作” 且 “可接受数据短暂不一致” 时使用(如配置缓存、静态列表);
  2. 避免大容量容器:数组过大时,复制操作会导致内存飙升和 GC 压力;
  3. 减少写操作频率:批量修改时尽量合并操作(如先收集所有修改,再一次性执行 addAll);
  4. 不依赖实时性:若业务要求 “写后立即可见”,需使用 ReentrantReadWriteLock 等强一致性方案。