CopyOnWriteArrayList 源码深度解析:从线程安全列表到写时复制的实现原理

一、道格·李为什么需要"写时复制"的列表

Java 1.0 的 Vectorsynchronized 保证线程安全——所有方法加锁,读写都互斥。Collections.synchronizedList 同理。这在读多写少的场景中有一个严重的浪费:多个线程同时读取不应该互相阻塞,因为读取不修改数据

但去掉锁也不行——ArrayList 的迭代器有 fail-fast 机制,遍历时如果有其他线程写入,直接抛 ConcurrentModificationException。而且多线程同时 add() 还会导致数据丢失(elementData 数组和 size 计数器都没有同步保护)。

道格·李在 JSR 166 中为这个场景设计了一个完全不同的策略:写时复制(Copy-On-Write)。每次写入(add、set、remove)不直接修改原数组,而是复制一份新数组,在新数组上操作,最后用 volatile 写把引用指向新数组。读操作完全无锁——直接读当前的数组引用,不需要任何同步。

这个设计的取舍非常明确:写操作很贵(要复制整个数组),但读操作极其便宜(无锁 + volatile 读)。因此 CopyOnWriteArrayList 仅适用于读多写极少(比如读:写 > 100:1)的场景——配置信息、监听器列表、白名单等写入很少但频繁遍历的数据结构。

📐 二、设计理念:Copy-On-Write

📌 2.1 什么是写时复制

写时复制(Copy-On-Write,COW)是一种并发优化策略。它的核心思想只有一句话:当容器需要被修改时,不直接在原数组上操作,而是先复制一份新数组,在新数组上修改,修改完成后用新数组替换旧数组的引用

这就保证了:读操作永远在不变的数组上进行,完全不需要加锁;写操作虽然开销大,但只影响它自己,不会阻塞任何读线程。

📌 2.2 CopyOnWriteArrayList 的架构总览

CopyOnWriteArrayList 的并发安全由"一锁一数组"两个组件配合完成:

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    COW[CopyOnWriteArrayList]
    COW --> ARRAY["array: volatile Object[]"]
    COW --> LOCK["lock: ReentrantLock"]

    ARRAY --> A0["元素0"]
    ARRAY --> A1["元素1"]
    ARRAY --> A2["元素2"]
    ARRAY --> AN["元素N"]

    LOCK --> W["写线程"]
    ARRAY --> R["读线程"]

    W --> |写操作加锁| LOCK
    R --> |volatile读无锁| ARRAY

class A0,A1,A2,AN,ARRAY,COW,LOCK,Object,R,W process;
组件类型作用
arrayvolatile Object[]存储元素,volatile 保证写后对其他线程立即可见
lockfinal ReentrantLock写操作的互斥锁,同一时刻只允许一个写线程

array 加上 volatile 是关键——写线程完成数组替换后,volatile 写语义将所有读线程看到的旧引用刷成新引用,保证最终一致性。

🔗 2.3 类继承关系

flowchart LR
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
    COW((CopyOnWriteArrayList))
    COW --> L[接口\nList]
    COW --> RA[接口\nRandomAccess]
    COW --> CL[接口\nCloneable]
    COW --> SE[接口\nSerializable]

class RA data;
class CL,L,SE process;
class COW startEnd;

CopyOnWriteArrayList 实现了四个接口:

接口含义
List<E>提供列表的标准增删改查 API
RandomAccess标记接口,表明底层基于数组,支持 O(1) 随机访问
Cloneable支持 clone() 创建浅拷贝
Serializable支持序列化与反序列化

对比它的"并发兄弟" CopyOnWriteArraySet,CopyOnWriteArraySet 内部直接持有一个 CopyOnWriteArrayList 实例,所有操作都委托给它——两者共享同一套写时复制的并发安全实现。

🔒 三、数据结构展开:volatile 数组 + ReentrantLock

⚙️ 3.1 核心字段

打开 JDK 源码,CopyOnWriteArrayList 的核心字段只有两个:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    /** 保证写操作的互斥 */
    final transient ReentrantLock lock = new ReentrantLock();

    /** 存储元素的数组,volatile 保证可见性 */
    private transient volatile Object[] array;
}

字段逐行解释:

  • lockfinal 修饰,构造时初始化一次,不可变。所有写操作(add、set、remove 等)必须先获取此锁。读操作不碰此锁。
  • arrayvolatile 修饰。volatile 的核心作用是:当一个线程调用 setArray(newArray) 后,其他所有线程随后通过 getArray() 读到的都是新数组引用,不会看到过期的旧引用。transient 意味着序列化时不会直接序列化此字段——CopyOnWriteArrayList 通过自定义 writeObject/readObject 处理。

📋 3.2 获取和设置数组的工具方法

final Object[] getArray() {
    return array;           // volatile 读
}

final void setArray(Object[] a) {
    array = a;              // volatile 写
}

这两个方法是 CopyOnWriteArrayList 内部操作数组的唯一入口。所有写操作最终都通过 setArray 原子性地替换数组引用,所有读操作都通过 getArray 读取当前快照

这里有一个重要的设计细节:lock 字段是通过 synchronized (lock) 或直接 lock.lock() 使用的,但 CopyOnWriteArrayList 还通过反射 + CAS 获取了 lock 字段的内存偏移量(lockOffset),用于 addIfAbsent 方法的优化。这一点在后面的源码分析中展开。

📖 四、源码分析:写操作的完整调用链

🔄 4.1 add(E e)——经典写时复制流程

add 是理解整个写时复制机制最好的入口。它的完整源码如下:

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();                    // ⑥ 释放锁
    }
}

逐行解读:

  • ① 获取锁ReentrantLock.lock() 保证写操作互斥。如果线程 A 正在 add,线程 B 的 add 会在这里阻塞。
  • ② 获取当前数组getArray()volatile 读,拿到此时最新的数组引用。
  • Arrays.copyOf:这是理解 COW 开销的关键。JDK 底层调用 System.arraycopy 将原数组的每个元素复制到长度 +1 的新数组。时间复杂度 O(n)。
  • ④ 赋值新元素:直接固定在 newElements[len] 位置。
  • ⑤ volatile 写setArray(newElements) 将 array 指向新数组。从此刻起,所有新发起的读请求都能看到新元素。
  • ⑥ 释放锁:无论是否发生异常,锁都会被释放。

时序图展示整个流程:

sequenceDiagram
    participant T1 as 写线程T1
    participant Lock as ReentrantLock
    participant Old as 旧数组
    participant New as 新数组
    participant T2 as 读线程T2

    T1->>Lock: lock()
    Lock-->>T1: 获取锁成功
    T1->>Old: getArray() 读取旧数组
    T1->>New: Arrays.copyOf 复制长度+1
    T1->>New: 新元素放入末尾
    T1->>Old: setArray(newArray) volatile写
    T1->>Lock: unlock()

    par 读线程并发
        T2->>Old: getArray() volatile读(锁释放后可见新数组)
    end

关键点:读线程 T2 在整个过程中没有等待任何锁。在 T1 执行 setArray 之前,T2 读到的还是旧数组(没有新元素);在 T1 执行 setArray 之后,T2 的 volatile 读就能看到新数组(包含新元素)。

📌 4.2 add(int index, E element)——指定位置插入

在指定位置插入元素的源码:

public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            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();
    }
}

与尾部 add 的区别在于数组复制策略:如果插入位置不是末尾,需要两次 System.arraycopy——先复制 [0, index) 段,再复制 [index, len) 段到偏移一位的位置。

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca,font-weight:bold;
    A[获取锁] --> B[getArray 获取当前数组]
    B --> C{index 越界检查}
    C -->|越界| D[抛出IndexOutOfBoundsException]
    C -->|合法| E{numMoved == 0?}
    E -->|是-尾部插入| F[Arrays.copyOf长度+1]
    E -->|否-中间插入| G[new Object len+1]
    G --> H[System.arraycopy 前段0到index-1]
    H --> I[System.arraycopy 后段index到len-1]
    F --> J[newElements index = element]
    I --> J
    J --> K[setArray 替换引用]
    K --> L[释放锁]

class C,E condition;
class A,B,F,G,H,I,J,K,L process;
class D reject;

📌 4.3 set(int index, E element)——替换指定位置元素

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 {
            // 新旧值相同,但为了 volatile 写语义,仍调用 setArray
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

注意这段代码中的细节:当 oldValue == element(新旧值是同一个引用),并没有直接跳过写操作,而是执行了 setArray(elements)。这个"空写"不是多余的——它保证了 volatile 的写语义。如果直接 return,其他线程可能因为缺少 volatile 写与后续 volatile 读之间的 happens-before 关系而看到过时状态。

📌 4.4 remove(int index)——删除指定位置元素

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)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            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();
    }
}

删除操作与 add(index, element) 的复制策略对称:

  • 删除最后一个元素numMoved == 0):直接用 Arrays.copyOf(elements, len - 1) 截断尾部。
  • 删除中间元素:创建 len - 1 长度的新数组,先复制 [0, index) 段,再复制 [index+1, len) 段(跳过被删元素)。

🔧 4.5 addIfAbsent(E e)——“不存在才添加"的并发安全实现

addIfAbsent 是 CopyOnWriteArrayList 中最复杂的方法。它的语义是:如果元素 e 不在列表中,则添加并返回 true;否则直接返回 false。这看起来像是 contains + add 的复合操作,但在并发环境下可能会有两个线程同时发现元素不存在,二者都添加——这正是需要 addIfAbsent 保证原子性的原因。

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {            // ① 快照已过时
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++) {
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;             // ② 已被其他线程添加
            }
            if (indexOf(e, current, common, len) >= 0)
                return false;                 // ③ 在超出部分中找到
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

这个方法的执行流程分为两个阶段:

阶段一(无锁预检):在加锁前,先用 indexOf 在快照 snapshot 上检查元素是否已存在。如果已存在,直接返回 false,避免了加锁的开销。这一步只是优化,不保证正确性(因为检查时无锁,快照可能过时)。

阶段二(加锁后双重检查):获取锁后,再次检查。重点在于 snapshot != current 这个判断:

  • 如果快照和当前数组相同(引用相等),说明在获取快照到加锁之间没有其他写操作,可以直接走到复制添加逻辑。
  • 如果快照和当前数组不同,说明这期间发生了写操作。此时需要遍历快照和历史数组的公共部分,对比差异:若某个位置 current[i] != snapshot[i]current[i].equals(e),说明目标元素 e 已被其他线程添加,直接返回 false。还不够——如果公共部分没找到差异,还要检查超出的那一段(indexOf(key, current, common, len)),因为新数组可能更长。
flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold;
    A[addIfAbsent E] --> B[获取快照snapshot]
    B --> C{indexOf快照中已存在?}
    C -->|是| D[return false]
    C -->|否| E[获取锁]
    E --> F[getArray 获取当前数组current]
    F --> G{snapshot == current?}
    G -->|相同| H[Arrays.copyOf 长度+1]
    G -->|不同-已被修改| I[遍历公共部分]
    I --> J{找到未变化位置\n且元素相等?}
    J -->|是| K[return false]
    J -->|否| L{剩余部分indexOf找到?}
    L -->|是| K
    L -->|否| H
    H --> M[setArray 替换引用]
    M --> N[释放锁]
    N --> O[return true]

class A,C,G,J,L condition;
class B,E,F,H,I,M,N process;
class D,K,O startEnd;

这个双重检查机制是 CopyOnWriteArrayList 中唯一一处涉及"比较新旧数组差异"的地方,也是它实现 Set 语义(CopyOnWriteArraySet 底层复用了这个方法)的核心逻辑。

5️⃣ 五、COWIterator:快照迭代器

⚙️ 5.1 快照机制的设计

CopyOnWriteArrayList 的迭代器 COWIterator 的核心设计是:创建迭代器时,直接持有当前数组引用的快照

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 boolean hasNext() {
        return cursor < snapshot.length;
    }

    public E next() {
        if (!hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
}

关键点:snapshot 不是复制出来的新数组,而是直接指向创建时刻的 array 引用。由于 CopyOnWriteArrayList 的写操作总是创建新数组然后替换引用,旧数组本身不会被修改。因此,这个 snapshot 引用在迭代器的整个生命周期中都是安全且不变的。

📌 5.2 弱一致性的具体表现

下面用一张时序图展示迭代器"看不到后续写入"的现象:

sequenceDiagram
    participant Main as 主线程
    participant List as CopyOnWriteArrayList
    participant Snap as 旧数组快照[A,B,C]
    participant Iter as COWIterator
    participant Write as 写线程

    Main->>List: add A,B,C
    Main->>List: 创建迭代器
    List->>Iter: COWIterator(snapshot=旧数组)
    Iter->>Snap: snapshot 指向旧数组
    par 写线程同时运行
        Write->>List: add D
        List->>List: 创建新数组[A,B,C,D]并setArray
    end
    Main->>Iter: hasNext() 仍读 snapshot
    Iter->>Snap: snapshot[0]=A
    Iter->>Snap: snapshot[1]=B
    Iter->>Snap: snapshot[2]=C
    Note over Iter,Write: 额外添加的D对迭代器不可见

📌 5.3 不支持写操作

COWIterator 的 remove()set()add() 全部抛出 UnsupportedOperationException

public void remove() {
    throw new UnsupportedOperationException();
}

public void set(E e) {
    throw new UnsupportedOperationException();
}

public void add(E e) {
    throw new UnsupportedOperationException();
}

这是由快照机制决定的——迭代器操作的是旧数组,如果在旧数组上修改,修改会被写线程的新数组覆盖,导致数据丢失。

六、与 Vector 的全面对比

Vector 和 CopyOnWriteArrayList 都是 List 的线程安全实现,但两者的安全策略完全不同。

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
    subgraph VEC[Vector 互斥锁策略]
        V1[synchronized add]
        V2[synchronized get]
        V3[synchronized remove]
        V4[复合操作需额外synchronized]
    end

    subgraph COW[CopyOnWriteArrayList 写时复制策略]
        C1[ReentrantLock add 复制数组]
        C2[volatile读 get 无锁]
        C3[ReentrantLock remove 复制数组]
        C4[COWIterator 快照迭代 无ConcurrentModificationException]
    end

class C4 condition;
class C1,C2,C3,COW,V1,V2,V3,V4,VEC process;
对比维度VectorCopyOnWriteArrayList
读操作加锁是,synchronized否,volatile 读直接返回
写操作实现直接在原数组操作复制新数组,在新数组操作后替换引用
迭代器安全性fail-fast,修改抛 ConcurrentModificationExceptionfail-safe,快照迭代,永不抛异常
迭代器写支持支持 remove()不支持,抛 UnsupportedOperationException
数据一致性强一致性最终一致性(弱一致性)
写操作内存开销低,原地修改高,每次复制整个数组
适用场景写操作较多读多写少

Vector 的读操作加锁是主要的性能瓶颈。即使是为了获取一个尺寸 size(),也要排他性地获取锁。而 CopyOnWriteArrayList 的 size() 只需要一次 volatile 读。

Collections.synchronizedList(new ArrayList<>()) 的情况与 Vector 相同——所有方法都通过 synchronized 包装,包括读方法。

🛠️ 七、日常开发中的常用方法

CopyOnWriteArrayList 作为 List 接口的实现,日常使用的 API 与 ArrayList 几乎相同,但因为线程安全特性的加持,在特定场景下是更好的选择。

方法用途频率
new CopyOnWriteArrayList<>()创建空列表
add(E e)末尾添加元素(会复制数组)
get(int index)按索引读取(无锁)
set(int index, E e)替换指定位置元素
remove(int index)按索引删除(会复制数组)
addIfAbsent(E e)不存在才添加(原子性保证)
iterator()获取快照迭代器
size()获取元素个数(无锁)
contains(Object o)判断元素是否存在(无锁)

💻 7.1 基本使用示例

// 创建
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

// 批量添加
list.add("redis");
list.add("mysql");
list.add("mongodb");

// 并发读——多个线程可以同时读,完全无锁
new Thread(() -> {
    for (String s : list) {
        System.out.println(s);
    }
}).start();

// 并发写——ReentrantLock 互斥
new Thread(() -> {
    list.add("elasticsearch");   // 内部复制整个数组
}).start();

// addIfAbsent——不存在才添加
boolean added = list.addIfAbsent("redis");    // false,已存在
boolean added2 = list.addIfAbsent("kafka");   // true,添加成功

🌐 7.2 典型的读多写少场景

public class EventListenerRegistry {
    // 监听器列表:注册/注销远少于事件通知
    private final CopyOnWriteArrayList<EventListener> listeners =
        new CopyOnWriteArrayList<>();

    public void register(EventListener listener) {
        listeners.addIfAbsent(listener);
    }

    public void unregister(EventListener listener) {
        listeners.remove(listener);
    }

    public void fireEvent(Event event) {
        // 读操作:无锁遍历,高频调用无性能瓶颈
        for (EventListener listener : listeners) {
            listener.onEvent(event);
        }
    }
}

在这个场景中,register / unregister 仅在系统启动或配置变更时偶尔触发,而 fireEvent 在每次业务请求时都会调用。使用 CopyOnWriteArrayList 避免了事件通知路径上的锁竞争。

📊 7.3 与 ArrayList 的现代用法对比

场景传统 ArrayList 写法CopyOnWriteArrayList 写法
普通遍历for (String s : list)相同,但迭代器为快照
遍历中删元素iterator.remove() 支持不支持,需另外收集后批量 removeAll
排序Collections.sort(list)不支持,内部数组不可变;需 toArray() 后排序再批量添加

🛠️ 八、使用注意事项

📌 8.1 内存开销:每次写操作都复制整个数组

这是 CopyOnWriteArrayList 最核心的代价。假设列表中有 10 万个元素,每次 add 都要复制这 10 万个元素到新数组。如果写操作频率较高,GC 压力会显著上升。

// 错误用法:列表较大时频繁写入
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100_000; i++) {
    list.add(i);   // 第i次add复制 i 个元素,累计O(n²)复制量
}

正确做法:如果初始化阶段有大量写入,应先用普通 ArrayList 完成批量操作,再转为 CopyOnWriteArrayList。

List<Integer> temp = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
    temp.add(i);
}
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(temp);

📌 8.2 数据一致性:读写之间是弱一致性

CopyOnWriteArrayList 只能保证 最终一致性,不能保证 实时一致性。以下场景会读到旧数据:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("old");

// 线程A:写操作
new Thread(() -> {
    list.set(0, "new");   // 复制数组并替换引用
}).start();

// 主线程:紧跟着读
System.out.println(list.get(0));  // 可能还是 "old"

set 操作包括:加锁 → 复制数组 → 修改元素 → volatile 写替换引用 → 释放锁。在 setArray 执行之前发起 get 的线程,读到的还是旧数组。

📌 8.3 迭代器不可修改

前面已经提到,COWIterator 的 remove()set()add() 都直接抛异常。如果在迭代过程中需要删除元素,必须在遍历完成后批量处理:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A"); list.add("B"); list.add("C");

// 错误写法
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s);   // 不会抛异常,但删除的是"当前数组"而非"快照数组"
    }
}

// 正确写法:先收集,再批量删除
List<String> toRemove = new ArrayList<>();
for (String s : list) {
    if ("B".equals(s)) {
        toRemove.add(s);
    }
}
list.removeAll(toRemove);

🌐 8.4 不适合数据量大的场景

在高性能互联网应用中,如果列表的数据量可能达到几千甚至上万,且存在写操作,使用 CopyOnWriteArrayList 可能导致:

  • Young GC / Full GC:每次写操作产生一个新的数组对象,旧数组被立即废弃。如果元素数量多,数组占用内存大(比如 10 万个引用 ≈ 0.8 MB),频繁写入会快速填满 Young 区,甚至触发 Full GC。
  • 写操作延迟抖动:数组复制的时间与元素数量成正比。在延迟敏感的系统中,一个 add 调用可能突然耗时数十毫秒,造成请求超时。

⚠️ 8.5 不适合排序操作

CopyOnWriteArrayList 会拒绝 Collections.sort()

CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(3); list.add(1); list.add(2);
Collections.sort(list);  // 抛出 UnsupportedOperationException

原因是 sort 内部会尝试调用 List.set(),但迭代器需要获取数组长度等数值并在原地写——这在 COW 的数据结构上无法高效实现。如果必须排序,需要先转为数组:

Integer[] arr = list.toArray(new Integer[0]);
Arrays.sort(arr);
CopyOnWriteArrayList<Integer> sorted = new CopyOnWriteArrayList<>(arr);

🎯 8.6 适用场景总结

场景是否适合说明
事件监听器注册列表适合注册/注销低频,事件通知高频
白名单/黑名单缓存适合更新频率极低,读取频率极高
系统配置项列表适合启动时写入,运行时只读
高并发写入队列不适合写操作复制整个数组,性能极差
大数据量列表(> 1 万条)不适合每次写复制大量数据,内存和 CPU 开销大
需要排序的列表不适合不支持 sort()

🎯 九、总结

CopyOnWriteArrayList 通过 volatile + ReentrantLock + 数组复制 三种机制的组合,实现了一种适合"读多写少"场景的线程安全列表。

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef highlight fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa,font-weight:bold;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
    subgraph CORE[核心机制]
        A[读操作: get/size/iterator] -->|无锁volatile读| B[直接读取当前array]
        C[写操作: add/set/remove] -->|ReentrantLock互斥| D[复制新数组]
        D --> E[在新数组上修改]
        E --> F[setArray volatile写替换引用]
    end

    subgraph ITER[迭代器]
        G[COWIterator] -->|快照机制| H[持有创建时的array引用]
        H --> I[迭代期间不变\n永不抛ConcurrentModificationException]
    end

    subgraph SCENE[适用场景]
        J[读多写少]
        K[数据量小]
        L[对一致性要求不高-最终一致]
    end

class I condition;
class K data;
class CORE highlight;
class A,B,C,D,E,F,G,H,ITER,J,L,SCENE process;
维度要点回顾
设计理念写时复制(COW):写时不修改原数组,而是复制新数组、修改、替换引用
数据结构volatile Object[] array + final ReentrantLock lock
读操作完全无锁,volatile 读保证可见性
写操作ReentrantLock 互斥,复制整个数组,时间复杂度 O(n)
迭代器COWIterator 快照机制,弱一致性,不支持 remove/set/add
与 Vector 对比读操作无锁性能显著优于 Vector,但写操作内存开销大
核心局限每次写操作复制整个数组,内存消耗大,不能保证实时一致性